Skip to content

Commit

Permalink
Add directory based archetypes
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Sep 19, 2018
1 parent 058cc6c commit 3c039b5
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 65 deletions.
31 changes: 25 additions & 6 deletions commands/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -100,21 +120,20 @@ 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]

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
}
Expand Down
204 changes: 162 additions & 42 deletions create/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -32,24 +35,34 @@ 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)
name := filepath.Base(targetPath)
return newContentFromDir(archetypeFilename, s, archetypeFs, s.Fs.Source, cm, name, 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)
}
defer f.Close()

if helpers.ReaderContains(f, []byte(".Site")) {
siteUsed = true
return err
}
}

Expand All @@ -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
}
Expand All @@ -103,29 +99,153 @@ func NewContent(
return nil
}

func newContentFromDir(
archetypeDir string,
s *hugolib.Site,
sourceFs, targetFs afero.Fs,
cm archetypeMap, name, 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, name, 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
}
22 changes: 9 additions & 13 deletions create/content_template_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,24 @@ var (
"%}x}", "%}}")
)

func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFilename string) ([]byte, error) {
func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archetypeFilename string) ([]byte, error) {

var (
archetypeContent []byte
archetypeTemplate []byte
err error
)

ps, err := helpers.NewPathSpec(s.Deps.Fs, s.Deps.Cfg)
if err != nil {
return nil, err
}
sp := source.NewSourceSpec(ps, ps.Fs.Source)

f := sp.NewFileInfo("", targetPath, false, nil)
f := s.SourceSpec.NewFileInfo("", targetPath, false, nil)

name := f.TranslationBaseName()
if name != "" {
name := f.TranslationBaseName()

if name == "index" || name == "_index" {
// Page bundles; the directory name will hopefully have a better name.
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
_, name = filepath.Split(dir)
if name == "index" || name == "_index" {
// Page bundles; the directory name will hopefully have a better name.
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
_, name = filepath.Split(dir)
}
}

data := ArchetypeFileData{
Expand Down
Loading

0 comments on commit 3c039b5

Please sign in to comment.