diff options
author | Bjørn Erik Pedersen <[email protected]> | 2018-09-19 07:48:17 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2018-09-23 19:27:23 +0200 |
commit | 2650fa772b40846d9965f8c5f169286411f3beb2 (patch) | |
tree | 26d209ebea23611c146d851cb12827e793eaf6d5 /create/content.go | |
parent | ef525b15d4584886b52428bd7a35de835ab07a48 (diff) | |
download | hugo-2650fa772b40846d9965f8c5f169286411f3beb2.tar.gz hugo-2650fa772b40846d9965f8c5f169286411f3beb2.zip |
Add directory based archetypes
Given this content:
```bash
archetypes
├── default.md
└── post-bundle
├── bio.md
├── images
│ └── featured.jpg
└── index.md
```
```bash
hugo new --kind post-bundle post/my-post
```
Will create a new folder in `/content/post/my-post` with the same set of files as in the `post-bundle` archetypes folder.
This commit also improves the archetype language detection, so, if you use template code in your content files, the `.Site` you get is for the correct language. This also means that it is now possible to translate strings defined in the `i18n` bundles, e.g. `{{ i18n "hello" }}`.
Fixes #4535
Diffstat (limited to 'create/content.go')
-rw-r--r-- | create/content.go | 269 |
1 files changed, 222 insertions, 47 deletions
diff --git a/create/content.go b/create/content.go index 6d022282e..00924941f 100644 --- a/create/content.go +++ b/create/content.go @@ -17,69 +17,74 @@ package create import ( "bytes" "fmt" + "io" "os" "os/exec" "path/filepath" + "strings" + + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" + "github.com/spf13/afero" jww "github.com/spf13/jwalterweatherman" ) // NewContent creates a new content file in the content directory based upon the // given kind, which is used to lookup an archetype. func NewContent( - ps *helpers.PathSpec, - siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error { + sites *hugolib.HugoSites, kind, targetPath string) error { + targetPath = filepath.Clean(targetPath) ext := helpers.Ext(targetPath) - fs := ps.BaseFs.SourceFilesystems.Archetypes.Fs + ps := sites.PathSpec + archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs + sourceFs := ps.Fs.Source jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext) - archetypeFilename := findArchetype(ps, kind, ext) + archetypeFilename, isDir := findArchetype(ps, kind, ext) + contentPath, s := resolveContentPath(sites, sourceFs, targetPath) - // Building the sites can be expensive, so only do it if really needed. - siteUsed := false + if isDir { - if archetypeFilename != "" { - f, err := fs.Open(archetypeFilename) + langFs := hugofs.NewLanguageFs(s.Language.Lang, sites.LanguageSet(), archetypeFs) + + cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename) if err != nil { - return fmt.Errorf("failed to open archetype file: %s", err) + return err } - defer f.Close() - if helpers.ReaderContains(f, []byte(".Site")) { - siteUsed = true + if cm.siteUsed { + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return err + } } - } - s, err := siteFactory(targetPath, siteUsed) - if err != nil { - return err + name := filepath.Base(targetPath) + return newContentFromDir(archetypeFilename, sites, archetypeFs, sourceFs, cm, name, contentPath) } - var content []byte + // Building the sites can be expensive, so only do it if really needed. + siteUsed := false - content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename) - if err != nil { - return err + if archetypeFilename != "" { + var err error + siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename) + if err != nil { + return err + } } - // The site may have multiple content dirs, and we currently do not know which contentDir the - // user wants to create this content in. We should improve on this, but we start by testing if the - // provided path points to an existing dir. If so, use it as is. - var contentPath string - var exists bool - targetDir := filepath.Dir(targetPath) - - if targetDir != "" && targetDir != "." { - exists, _ = helpers.Exists(targetDir, fs) + if siteUsed { + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return err + } } - if exists { - contentPath = targetPath - } else { - contentPath = s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath)) + content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename) + if err != nil { + return err } if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil { @@ -103,29 +108,199 @@ func NewContent( return nil } +func targetSite(sites *hugolib.HugoSites, fi *hugofs.LanguageFileInfo) *hugolib.Site { + for _, s := range sites.Sites { + if fi.Lang() == s.Language.Lang { + return s + } + } + return sites.Sites[0] +} + +func newContentFromDir( + archetypeDir string, + sites *hugolib.HugoSites, + sourceFs, targetFs afero.Fs, + cm archetypeMap, name, targetPath string) error { + + for _, f := range cm.otherFiles { + filename := f.Filename() + // Just copy the file to destination. + in, err := sourceFs.Open(filename) + if err != nil { + return err + } + + targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) + + targetDir := filepath.Dir(targetFilename) + if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create target directory for %s: %s", targetDir, err) + } + + out, err := targetFs.Create(targetFilename) + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + in.Close() + out.Close() + } + + for _, f := range cm.contentFiles { + filename := f.Filename() + s := targetSite(sites, f) + targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) + + content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename) + if err != nil { + return err + } + + if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil { + return err + } + } + + jww.FEEDBACK.Println(targetPath, "created") + + return nil +} + +type archetypeMap struct { + // These needs to be parsed and executed as Go templates. + contentFiles []*hugofs.LanguageFileInfo + // These are just copied to destination. + otherFiles []*hugofs.LanguageFileInfo + // If the templates needs a fully built site. This can potentially be + // expensive, so only do when needed. + siteUsed bool +} + +func mapArcheTypeDir( + ps *helpers.PathSpec, + fs afero.Fs, + archetypeDir string) (archetypeMap, error) { + + var m archetypeMap + + walkFn := func(filename string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + if fi.IsDir() { + return nil + } + + fil := fi.(*hugofs.LanguageFileInfo) + + if hugolib.IsContentFile(filename) { + m.contentFiles = append(m.contentFiles, fil) + if !m.siteUsed { + m.siteUsed, err = usesSiteVar(fs, filename) + if err != nil { + return err + } + } + return nil + } + + m.otherFiles = append(m.otherFiles, fil) + + return nil + } + + if err := helpers.SymbolicWalk(fs, archetypeDir, walkFn); err != nil { + return m, err + } + + return m, nil +} + +func usesSiteVar(fs afero.Fs, filename string) (bool, error) { + f, err := fs.Open(filename) + if err != nil { + return false, fmt.Errorf("failed to open archetype file: %s", err) + } + defer f.Close() + return helpers.ReaderContains(f, []byte(".Site")), nil +} + +// Resolve the target content path. +func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) { + targetDir := filepath.Dir(targetPath) + first := sites.Sites[0] + + var ( + s *hugolib.Site + siteContentDir string + ) + + // Try the filename: my-post.en.md + for _, ss := range sites.Sites { + if strings.Contains(targetPath, "."+ss.Language.Lang+".") { + s = ss + break + } + } + + for _, ss := range sites.Sites { + contentDir := ss.PathSpec.ContentDir + if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) { + contentDir += helpers.FilePathSeparator + } + if strings.HasPrefix(targetPath, contentDir) { + siteContentDir = ss.PathSpec.ContentDir + if s == nil { + s = ss + } + break + } + } + + if s == nil { + s = first + } + + if targetDir != "" && targetDir != "." { + exists, _ := helpers.Exists(targetDir, fs) + + if exists { + return targetPath, s + } + } + + if siteContentDir != "" { + pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir)) + return s.PathSpec.AbsPathify(pp), s + + } else { + return s.PathSpec.AbsPathify(filepath.Join(first.PathSpec.ContentDir, targetPath)), s + } + +} + // FindArchetype takes a given kind/archetype of content and returns the path // to the archetype in the archetype filesystem, blank if none found. -func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string) { +func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) { fs := ps.BaseFs.Archetypes.Fs - // If the new content isn't in a subdirectory, kind == "". - // Therefore it should be excluded otherwise `is a directory` - // error will occur. github.com/gohugoio/hugo/issues/411 - var pathsToCheck = []string{"default"} + var pathsToCheck []string - if ext != "" { - if kind != "" { - pathsToCheck = append([]string{kind + ext, "default" + ext}, pathsToCheck...) - } else { - pathsToCheck = append([]string{"default" + ext}, pathsToCheck...) - } + if kind != "" { + pathsToCheck = append(pathsToCheck, kind+ext) } + pathsToCheck = append(pathsToCheck, "default"+ext, "default") for _, p := range pathsToCheck { - if exists, _ := helpers.Exists(p, fs); exists { - return p + fi, err := fs.Stat(p) + if err == nil { + return p, fi.IsDir() } } - return "" + return "", false } |