Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add directory based archetypes
Browse files Browse the repository at this point in the history
bep committed Sep 19, 2018

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 058cc6c commit 8eede61
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
@@ -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,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
}
204 changes: 162 additions & 42 deletions create/content.go
Original file line number Diff line number Diff line change
@@ -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,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
}
}

@@ -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, 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, "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
}
22 changes: 9 additions & 13 deletions create/content_template_handler.go
Original file line number Diff line number Diff line change
@@ -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{
41 changes: 41 additions & 0 deletions create/content_test.go
Original file line number Diff line number Diff line change
@@ -79,6 +79,41 @@ 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 }}
Name: {{ replace .Name "-" " " | title }}
`

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`, `Name: My Post`)
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`)

}

func initViper(v *viper.Viper) {
v.Set("metaDataFormat", "toml")
v.Set("archetypeDir", "archetypes")
@@ -166,6 +201,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)
4 changes: 2 additions & 2 deletions hugolib/fileInfo.go
Original file line number Diff line number Diff line change
@@ -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.") {
2 changes: 1 addition & 1 deletion hugolib/page_bundler_capture.go
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion hugolib/site.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 8eede61

Please sign in to comment.