diff --git a/commands/new.go b/commands/new.go index e7065851160..62893b01204 100644 --- a/commands/new.go +++ b/commands/new.go @@ -86,6 +86,26 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error { var kind string createPath, kind = newContentPathSection(createPath) + hasDot := strings.Contains(createPath, ".") + + langMatch := func(s *hugolib.Site) bool { + lang := s.Language.Lang + + if hasDot { + // Most likely a file. + if strings.Contains(createPath, "."+lang) { + return true + } + } + + // Check for language content dir + contentDir := filepath.ToSlash(s.PathSpec.ContentDir) + if strings.HasPrefix(createPath, contentDir) { + return true + } + + return false + } if n.contentType != "" { kind = n.contentType @@ -100,13 +120,12 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error { // If a site isn't in use in the archetype template, we can skip the build. siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { - if !siteUsed { - return hugolib.NewSite(*cfg) - } var s *hugolib.Site - if err := c.hugo.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return nil, err + if siteUsed { + if err := c.hugo.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return nil, err + } } s = c.hugo.Sites[0] @@ -114,7 +133,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error { if len(c.hugo.Sites) > 1 { // Find the best match. for _, ss := range c.hugo.Sites { - if strings.Contains(createPath, "."+ss.Language.Lang) { + if langMatch(ss) { s = ss break } diff --git a/create/content.go b/create/content.go index 6d022282e25..a47ad28e0d6 100644 --- a/create/content.go +++ b/create/content.go @@ -17,12 +17,15 @@ package create import ( "bytes" "fmt" + "io" "os" "os/exec" "path/filepath" + "strings" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" + "github.com/spf13/afero" jww "github.com/spf13/jwalterweatherman" ) @@ -32,25 +35,35 @@ func NewContent( ps *helpers.PathSpec, siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error { ext := helpers.Ext(targetPath) - fs := ps.BaseFs.SourceFilesystems.Archetypes.Fs + archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs 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) + + if isDir { + cm, err := mapArcheTypeDir(ps, archetypeFs, archetypeFilename) + if err != nil { + return err + } + s, err := siteFactory(targetPath, cm.siteUsed) + if err != nil { + return err + } + contentPath := resolveContentPath(s, s.Fs.Source, targetPath) + return newContentFromDir(archetypeFilename, s, archetypeFs, s.Fs.Source, cm, contentPath) + } // Building the sites can be expensive, so only do it if really needed. siteUsed := false if archetypeFilename != "" { - f, err := fs.Open(archetypeFilename) + var err error + siteUsed, err = usesSiteVar(archetypeFs, 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 - } } s, err := siteFactory(targetPath, siteUsed) @@ -58,30 +71,13 @@ func NewContent( return err } - var content []byte + contentPath := resolveContentPath(s, archetypeFs, targetPath) - content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename) + content, err := executeArcheTypeAsTemplate(s, kind, targetPath, 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 exists { - contentPath = targetPath - } else { - contentPath = s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath)) - } - if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil { return err } @@ -103,29 +99,153 @@ func NewContent( return nil } +func newContentFromDir( + archetypeDir string, + s *hugolib.Site, + sourceFs, targetFs afero.Fs, + cm archetypeMap, targetPath string) error { + + for _, filename := range cm.otherFiles { + // Just copy the file to destination. + in, err := sourceFs.Open(filepath.Join(archetypeDir, filename)) + if err != nil { + return err + } + + targetFilename := filepath.Join(targetPath, filename) + 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 _, filename := range cm.contentFiles { + sourceFilename := filepath.Join(archetypeDir, filename) + targetFilename := filepath.Join(targetPath, filename) + content, err := executeArcheTypeAsTemplate(s, archetypeDir, targetFilename, sourceFilename) + 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 []string + // These are just copied to destination. + otherFiles []string + // 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 + } + + relfilename := strings.TrimPrefix(filename, archetypeDir) + + if hugolib.IsContentFile(filename) { + m.contentFiles = append(m.contentFiles, relfilename) + if !m.siteUsed { + m.siteUsed, err = usesSiteVar(fs, filename) + if err != nil { + return err + } + } + return nil + } + + m.otherFiles = append(m.otherFiles, relfilename) + + 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 +} + +func resolveContentPath(s *hugolib.Site, fs afero.Fs, targetPath string) string { + // 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 exists { + contentPath = targetPath + } else { + contentPath = s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath)) + } + + return contentPath +} + // 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) 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 } diff --git a/create/content_test.go b/create/content_test.go index f3bcc1dd561..9c08601c093 100644 --- a/create/content_test.go +++ b/create/content_test.go @@ -79,6 +79,40 @@ func TestNewContent(t *testing.T) { } } +func TestNewContentFromDir(t *testing.T) { + assert := require.New(t) + cfg, fs := newTestCfg() + assert.NoError(initFs(fs)) + + archetypeDir := filepath.Join("archetypes", "my-bundle") + assert.NoError(fs.Source.Mkdir(archetypeDir, 0755)) + + contentFile := ` +File: %s +Site Lang: {{ .Site.Language.Lang }} +` + + assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755)) + assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755)) + assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755)) + assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755)) + + h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs}) + assert.NoError(err) + + siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { + return h.Sites[0], nil + } + + assert.NoError(create.NewContent(h.PathSpec, siteFactory, "my-bundle", "post/my-post")) + + assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) + assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`) + assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`) + assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`) + +} + func initViper(v *viper.Viper) { v.Set("metaDataFormat", "toml") v.Set("archetypeDir", "archetypes") @@ -166,6 +200,12 @@ Some text. return nil } +func assertContains(assert *require.Assertions, v interface{}, matches ...string) { + for _, m := range matches { + assert.Contains(v, m) + } +} + // TODO(bep) extract common testing package with this and some others func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { filename = filepath.FromSlash(filename) diff --git a/hugolib/fileInfo.go b/hugolib/fileInfo.go index 90cf9137712..e4af42fd34b 100644 --- a/hugolib/fileInfo.go +++ b/hugolib/fileInfo.go @@ -61,7 +61,7 @@ func (fi *fileInfo) isOwner() bool { return fi.bundleTp > bundleNot } -func isContentFile(filename string) bool { +func IsContentFile(filename string) bool { return contentFileExtensionsSet[strings.TrimPrefix(helpers.Ext(filename), ".")] } @@ -98,7 +98,7 @@ const ( // Returns the given file's name's bundle type and whether it is a content // file or not. func classifyBundledFile(name string) (bundleDirType, bool) { - if !isContentFile(name) { + if !IsContentFile(name) { return bundleNot, false } if strings.HasPrefix(name, "_index.") { diff --git a/hugolib/page_bundler_capture.go b/hugolib/page_bundler_capture.go index 6fe413014a1..fbfad0103a2 100644 --- a/hugolib/page_bundler_capture.go +++ b/hugolib/page_bundler_capture.go @@ -76,7 +76,7 @@ func newCapturer( isBundleHeader := func(filename string) bool { base := filepath.Base(filename) name := helpers.Filename(base) - return isContentFile(base) && (name == "index" || name == "_index") + return IsContentFile(base) && (name == "index" || name == "_index") } // Make sure that any bundle header files are processed before the others. This makes diff --git a/hugolib/site.go b/hugolib/site.go index 59f0a9002bd..e71a6fdc15c 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -795,7 +795,7 @@ func (s *Site) processPartial(events []fsnotify.Event) (whatChanged, error) { removed = true } } - if removed && isContentFile(ev.Name) { + if removed && IsContentFile(ev.Name) { h.removePageByFilename(ev.Name) }