paths.go (7600B)
1 // Copyright 2018 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package paths
15
16 import (
17 "fmt"
18 "path/filepath"
19 "strings"
20
21 hpaths "github.com/gohugoio/hugo/common/paths"
22
23 "github.com/gohugoio/hugo/config"
24 "github.com/gohugoio/hugo/langs"
25 "github.com/gohugoio/hugo/modules"
26
27 "github.com/gohugoio/hugo/hugofs"
28 )
29
30 var FilePathSeparator = string(filepath.Separator)
31
32 type Paths struct {
33 Fs *hugofs.Fs
34 Cfg config.Provider
35
36 BaseURL
37 BaseURLString string
38 BaseURLNoPathString string
39
40 // If the baseURL contains a base path, e.g. https://example.com/docs, then "/docs" will be the BasePath.
41 BasePath string
42
43 // Directories
44 // TODO(bep) when we have trimmed down most of the dirs usage outside of this package, make
45 // these into an interface.
46 ThemesDir string
47 WorkingDir string
48
49 // Directories to store Resource related artifacts.
50 AbsResourcesDir string
51
52 AbsPublishDir string
53
54 // pagination path handling
55 PaginatePath string
56
57 // When in multihost mode, this returns a list of base paths below PublishDir
58 // for each language.
59 MultihostTargetBasePaths []string
60
61 DisablePathToLower bool
62 RemovePathAccents bool
63 UglyURLs bool
64 CanonifyURLs bool
65
66 Language *langs.Language
67 Languages langs.Languages
68 LanguagesDefaultFirst langs.Languages
69
70 // The PathSpec looks up its config settings in both the current language
71 // and then in the global Viper config.
72 // Some settings, the settings listed below, does not make sense to be set
73 // on per-language-basis. We have no good way of protecting against this
74 // other than a "white-list". See language.go.
75 defaultContentLanguageInSubdir bool
76 DefaultContentLanguage string
77 multilingual bool
78
79 AllModules modules.Modules
80 ModulesClient *modules.Client
81 }
82
83 func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
84 baseURLstr := cfg.GetString("baseURL")
85 baseURL, err := newBaseURLFromString(baseURLstr)
86 if err != nil {
87 return nil, fmt.Errorf("Failed to create baseURL from %q:: %w", baseURLstr, err)
88 }
89
90 contentDir := filepath.Clean(cfg.GetString("contentDir"))
91 workingDir := filepath.Clean(cfg.GetString("workingDir"))
92 resourceDir := filepath.Clean(cfg.GetString("resourceDir"))
93 publishDir := filepath.Clean(cfg.GetString("publishDir"))
94
95 if publishDir == "" {
96 return nil, fmt.Errorf("publishDir not set")
97 }
98
99 defaultContentLanguage := cfg.GetString("defaultContentLanguage")
100
101 var (
102 language *langs.Language
103 languages langs.Languages
104 languagesDefaultFirst langs.Languages
105 )
106
107 if l, ok := cfg.(*langs.Language); ok {
108 language = l
109 }
110
111 if l, ok := cfg.Get("languagesSorted").(langs.Languages); ok {
112 languages = l
113 }
114
115 if l, ok := cfg.Get("languagesSortedDefaultFirst").(langs.Languages); ok {
116 languagesDefaultFirst = l
117 }
118
119 //
120
121 if len(languages) == 0 {
122 // We have some old tests that does not test the entire chain, hence
123 // they have no languages. So create one so we get the proper filesystem.
124 languages = langs.Languages{&langs.Language{Lang: "en", Cfg: cfg, ContentDir: contentDir}}
125 }
126
127 absPublishDir := hpaths.AbsPathify(workingDir, publishDir)
128 if !strings.HasSuffix(absPublishDir, FilePathSeparator) {
129 absPublishDir += FilePathSeparator
130 }
131 // If root, remove the second '/'
132 if absPublishDir == "//" {
133 absPublishDir = FilePathSeparator
134 }
135 absResourcesDir := hpaths.AbsPathify(workingDir, resourceDir)
136 if !strings.HasSuffix(absResourcesDir, FilePathSeparator) {
137 absResourcesDir += FilePathSeparator
138 }
139 if absResourcesDir == "//" {
140 absResourcesDir = FilePathSeparator
141 }
142
143 var multihostTargetBasePaths []string
144 if languages.IsMultihost() {
145 for _, l := range languages {
146 multihostTargetBasePaths = append(multihostTargetBasePaths, l.Lang)
147 }
148 }
149
150 var baseURLString = baseURL.String()
151 var baseURLNoPath = baseURL.URL()
152 baseURLNoPath.Path = ""
153 var baseURLNoPathString = baseURLNoPath.String()
154
155 p := &Paths{
156 Fs: fs,
157 Cfg: cfg,
158 BaseURL: baseURL,
159 BaseURLString: baseURLString,
160 BaseURLNoPathString: baseURLNoPathString,
161
162 DisablePathToLower: cfg.GetBool("disablePathToLower"),
163 RemovePathAccents: cfg.GetBool("removePathAccents"),
164 UglyURLs: cfg.GetBool("uglyURLs"),
165 CanonifyURLs: cfg.GetBool("canonifyURLs"),
166
167 ThemesDir: cfg.GetString("themesDir"),
168 WorkingDir: workingDir,
169
170 AbsResourcesDir: absResourcesDir,
171 AbsPublishDir: absPublishDir,
172
173 multilingual: cfg.GetBool("multilingual"),
174 defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"),
175 DefaultContentLanguage: defaultContentLanguage,
176
177 Language: language,
178 Languages: languages,
179 LanguagesDefaultFirst: languagesDefaultFirst,
180 MultihostTargetBasePaths: multihostTargetBasePaths,
181
182 PaginatePath: cfg.GetString("paginatePath"),
183 }
184
185 if cfg.IsSet("allModules") {
186 p.AllModules = cfg.Get("allModules").(modules.Modules)
187 }
188
189 if cfg.IsSet("modulesClient") {
190 p.ModulesClient = cfg.Get("modulesClient").(*modules.Client)
191 }
192
193 return p, nil
194 }
195
196 // GetBasePath returns any path element in baseURL if needed.
197 func (p *Paths) GetBasePath(isRelativeURL bool) string {
198 if isRelativeURL && p.CanonifyURLs {
199 // The baseURL will be prepended later.
200 return ""
201 }
202 return p.BasePath
203 }
204
205 func (p *Paths) Lang() string {
206 if p == nil || p.Language == nil {
207 return ""
208 }
209 return p.Language.Lang
210 }
211
212 func (p *Paths) GetTargetLanguageBasePath() string {
213 if p.Languages.IsMultihost() {
214 // In a multihost configuration all assets will be published below the language code.
215 return p.Lang()
216 }
217 return p.GetLanguagePrefix()
218 }
219
220 func (p *Paths) GetURLLanguageBasePath() string {
221 if p.Languages.IsMultihost() {
222 return ""
223 }
224 return p.GetLanguagePrefix()
225 }
226
227 func (p *Paths) GetLanguagePrefix() string {
228 if !p.multilingual {
229 return ""
230 }
231
232 defaultLang := p.DefaultContentLanguage
233 defaultInSubDir := p.defaultContentLanguageInSubdir
234
235 currentLang := p.Language.Lang
236 if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
237 return ""
238 }
239 return currentLang
240 }
241
242 // GetLangSubDir returns the given language's subdir if needed.
243 func (p *Paths) GetLangSubDir(lang string) string {
244 if !p.multilingual {
245 return ""
246 }
247
248 if p.Languages.IsMultihost() {
249 return ""
250 }
251
252 if lang == "" || (lang == p.DefaultContentLanguage && !p.defaultContentLanguageInSubdir) {
253 return ""
254 }
255
256 return lang
257 }
258
259 // AbsPathify creates an absolute path if given a relative path. If already
260 // absolute, the path is just cleaned.
261 func (p *Paths) AbsPathify(inPath string) string {
262 return hpaths.AbsPathify(p.WorkingDir, inPath)
263 }
264
265 // RelPathify trims any WorkingDir prefix from the given filename. If
266 // the filename is not considered to be absolute, the path is just cleaned.
267 func (p *Paths) RelPathify(filename string) string {
268 filename = filepath.Clean(filename)
269 if !filepath.IsAbs(filename) {
270 return filename
271 }
272
273 return strings.TrimPrefix(strings.TrimPrefix(filename, p.WorkingDir), FilePathSeparator)
274 }