github.com/neohugo/neohugo@v0.123.8/create/content.go (about) 1 // Copyright 2019 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 create provides functions to create new content. 15 package create 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "path/filepath" 24 "strings" 25 26 "github.com/neohugo/neohugo/hugofs/glob" 27 28 "github.com/neohugo/neohugo/common/hexec" 29 "github.com/neohugo/neohugo/common/hstrings" 30 "github.com/neohugo/neohugo/common/paths" 31 32 "github.com/neohugo/neohugo/hugofs" 33 "github.com/neohugo/neohugo/hugofs/files" 34 35 "github.com/neohugo/neohugo/helpers" 36 "github.com/neohugo/neohugo/hugolib" 37 "github.com/spf13/afero" 38 ) 39 40 const ( 41 // DefaultArchetypeTemplateTemplate is the template used in 'hugo new site' 42 // and the template we use as a fall back. 43 DefaultArchetypeTemplateTemplate = `--- 44 title: "{{ replace .File.ContentBaseName "-" " " | title }}" 45 date: {{ .Date }} 46 draft: true 47 --- 48 49 ` 50 ) 51 52 // NewContent creates a new content file in h (or a full bundle if the archetype is a directory) 53 // in targetPath. 54 func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error { 55 if _, err := h.BaseFs.Content.Fs.Stat(""); err != nil { 56 return errors.New("no existing content directory configured for this project") 57 } 58 59 cf := hugolib.NewContentFactory(h) 60 61 if kind == "" { 62 var err error 63 kind, err = cf.SectionFromFilename(targetPath) 64 if err != nil { 65 return err 66 } 67 } 68 69 b := &contentBuilder{ 70 archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs, 71 sourceFs: h.PathSpec.Fs.Source, 72 ps: h.PathSpec, 73 h: h, 74 cf: cf, 75 76 kind: kind, 77 targetPath: targetPath, 78 force: force, 79 } 80 81 ext := paths.Ext(targetPath) 82 83 b.setArcheTypeFilenameToUse(ext) 84 85 withBuildLock := func() (string, error) { 86 unlock, err := h.BaseFs.LockBuild() 87 if err != nil { 88 return "", fmt.Errorf("failed to acquire a build lock: %s", err) 89 } 90 defer unlock() 91 92 if b.isDir { 93 return "", b.buildDir() 94 } 95 96 if ext == "" { 97 return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath) 98 } 99 100 if !files.IsContentFile(b.targetPath) { 101 return "", fmt.Errorf("target path %q is not a known content format", b.targetPath) 102 } 103 104 return b.buildFile() 105 } 106 107 filename, err := withBuildLock() 108 if err != nil { 109 return err 110 } 111 112 if filename != "" { 113 return b.openInEditorIfConfigured(filename) 114 } 115 116 return nil 117 } 118 119 type contentBuilder struct { 120 archeTypeFs afero.Fs 121 sourceFs afero.Fs 122 123 ps *helpers.PathSpec 124 h *hugolib.HugoSites 125 cf hugolib.ContentFactory 126 127 // Builder state 128 archetypeFi hugofs.FileMetaInfo 129 targetPath string 130 kind string 131 isDir bool 132 dirMap archetypeMap 133 force bool 134 } 135 136 func (b *contentBuilder) buildDir() error { 137 // Split the dir into content files and the rest. 138 if err := b.mapArcheTypeDir(); err != nil { 139 return err 140 } 141 142 var contentTargetFilenames []string 143 var baseDir string 144 145 for _, fi := range b.dirMap.contentFiles { 146 147 targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().PathInfo.Path(), b.archetypeFi.Meta().PathInfo.Path())) 148 149 // ===> post/my-post/pages/bio.md 150 abs, err := b.cf.CreateContentPlaceHolder(targetFilename, b.force) 151 if err != nil { 152 return err 153 } 154 if baseDir == "" { 155 baseDir = strings.TrimSuffix(abs, targetFilename) 156 } 157 158 contentTargetFilenames = append(contentTargetFilenames, abs) 159 } 160 161 var contentInclusionFilter *glob.FilenameFilter 162 if !b.dirMap.siteUsed { 163 // We don't need to build everything. 164 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { 165 filename = strings.TrimPrefix(filename, string(os.PathSeparator)) 166 for _, cn := range contentTargetFilenames { 167 if strings.Contains(cn, filename) { 168 return true 169 } 170 } 171 return false 172 }) 173 } 174 175 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { 176 return err 177 } 178 179 for i, filename := range contentTargetFilenames { 180 if err := b.applyArcheType(filename, b.dirMap.contentFiles[i]); err != nil { 181 return err 182 } 183 } 184 185 // Copy the rest as is. 186 for _, fi := range b.dirMap.otherFiles { 187 meta := fi.Meta() 188 189 in, err := meta.Open() 190 if err != nil { 191 return fmt.Errorf("failed to open non-content file: %w", err) 192 } 193 targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(fi.Meta().Filename, b.archetypeFi.Meta().Filename)) 194 targetDir := filepath.Dir(targetFilename) 195 196 if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) { 197 return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err) 198 } 199 200 out, err := b.sourceFs.Create(targetFilename) 201 if err != nil { 202 return err 203 } 204 205 _, err = io.Copy(out, in) 206 if err != nil { 207 return err 208 } 209 210 in.Close() 211 out.Close() 212 } 213 214 b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath)) 215 216 return nil 217 } 218 219 func (b *contentBuilder) buildFile() (string, error) { 220 contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force) 221 if err != nil { 222 return "", err 223 } 224 225 usesSite, err := b.usesSiteVar(b.archetypeFi) 226 if err != nil { 227 return "", err 228 } 229 230 var contentInclusionFilter *glob.FilenameFilter 231 if !usesSite { 232 // We don't need to build everything. 233 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { 234 filename = strings.TrimPrefix(filename, string(os.PathSeparator)) 235 return strings.Contains(contentPlaceholderAbsFilename, filename) 236 }) 237 } 238 239 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { 240 return "", err 241 } 242 243 if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFi); err != nil { 244 return "", err 245 } 246 247 b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename) 248 249 return contentPlaceholderAbsFilename, nil 250 } 251 252 func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) { 253 var pathsToCheck []string 254 255 if b.kind != "" { 256 pathsToCheck = append(pathsToCheck, b.kind+ext) 257 } 258 259 pathsToCheck = append(pathsToCheck, "default"+ext) 260 261 for _, p := range pathsToCheck { 262 fi, err := b.archeTypeFs.Stat(p) 263 if err == nil { 264 b.archetypeFi = fi.(hugofs.FileMetaInfo) 265 b.isDir = fi.IsDir() 266 return 267 } 268 } 269 } 270 271 func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugofs.FileMetaInfo) error { 272 p := b.h.GetContentPage(contentFilename) 273 if p == nil { 274 panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename)) 275 } 276 277 f, err := b.sourceFs.Create(contentFilename) 278 if err != nil { 279 return err 280 } 281 defer f.Close() 282 283 if archetypeFi == nil { 284 return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate) 285 } 286 287 return b.cf.ApplyArchetypeFi(f, p, b.kind, archetypeFi) 288 } 289 290 func (b *contentBuilder) mapArcheTypeDir() error { 291 var m archetypeMap 292 293 seen := map[hstrings.Tuple]bool{} 294 295 walkFn := func(path string, fim hugofs.FileMetaInfo) error { 296 if fim.IsDir() { 297 return nil 298 } 299 300 pi := fim.Meta().PathInfo 301 302 if pi.IsContent() { 303 pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang} 304 if seen[pathLang] { 305 // Duplicate content file, e.g. page.md and page.html. 306 // In the regular build, we will filter out the duplicates, but 307 // for archetype folders these are ambiguous and we need to 308 // fail. 309 return fmt.Errorf("duplicate content file found in archetype folder: %q; having both e.g. %s.md and %s.html is ambigous", path, pi.BaseNameNoIdentifier(), pi.BaseNameNoIdentifier()) 310 } 311 seen[pathLang] = true 312 m.contentFiles = append(m.contentFiles, fim) 313 if !m.siteUsed { 314 var err error 315 m.siteUsed, err = b.usesSiteVar(fim) 316 if err != nil { 317 return err 318 } 319 } 320 return nil 321 } 322 323 m.otherFiles = append(m.otherFiles, fim) 324 325 return nil 326 } 327 328 walkCfg := hugofs.WalkwayConfig{ 329 WalkFn: walkFn, 330 Fs: b.archeTypeFs, 331 Root: filepath.FromSlash(b.archetypeFi.Meta().PathInfo.Path()), 332 } 333 334 w := hugofs.NewWalkway(walkCfg) 335 336 if err := w.Walk(); err != nil { 337 return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFi.Meta().Filename, err) 338 } 339 340 b.dirMap = m 341 342 return nil 343 } 344 345 func (b *contentBuilder) openInEditorIfConfigured(filename string) error { 346 editor := b.h.Conf.NewContentEditor() 347 if editor == "" { 348 return nil 349 } 350 351 editorExec := strings.Fields(editor)[0] 352 editorFlags := strings.Fields(editor)[1:] 353 354 var args []any 355 for _, editorFlag := range editorFlags { 356 args = append(args, editorFlag) 357 } 358 args = append( 359 args, 360 filename, 361 hexec.WithStdin(os.Stdin), 362 hexec.WithStderr(os.Stderr), 363 hexec.WithStdout(os.Stdout), 364 ) 365 366 b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec) 367 368 cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...) 369 if err != nil { 370 return err 371 } 372 373 return cmd.Run() 374 } 375 376 func (b *contentBuilder) usesSiteVar(fi hugofs.FileMetaInfo) (bool, error) { 377 if fi == nil { 378 return false, nil 379 } 380 f, err := fi.Meta().Open() 381 if err != nil { 382 return false, err 383 } 384 defer f.Close() 385 bb, err := io.ReadAll(f) 386 if err != nil { 387 return false, fmt.Errorf("failed to read archetype file: %w", err) 388 } 389 390 return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil 391 } 392 393 type archetypeMap struct { 394 // These needs to be parsed and executed as Go templates. 395 contentFiles []hugofs.FileMetaInfo 396 // These are just copied to destination. 397 otherFiles []hugofs.FileMetaInfo 398 // If the templates needs a fully built site. This can potentially be 399 // expensive, so only do when needed. 400 siteUsed bool 401 }