The old implementation had some issues, mostly related to the context (e.g. name, file paths) passed to the template. This new implementation is using the exact same code path for evaluating the pages as in a regular build. This also makes it more robust and easier to reason about in a multilingual setup. Now, if you are explicit about the target path, Hugo will now always pick the correct mount and language: ```bash hugo new content/en/posts/my-first-post.md ``` Fixes #9032 Fixes #7589 Fixes #9043 Fixes #9046 Fixes #9047
343 lines
7.9 KiB
Go
343 lines
7.9 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package create provides functions to create new content.
|
|
package create
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gohugoio/hugo/hugofs/glob"
|
|
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/gohugoio/hugo/common/hexec"
|
|
"github.com/gohugoio/hugo/hugofs/files"
|
|
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/hugolib"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
const (
|
|
// DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
|
|
// and the template we use as a fall back.
|
|
DefaultArchetypeTemplateTemplate = `---
|
|
title: "{{ replace .Name "-" " " | title }}"
|
|
date: {{ .Date }}
|
|
draft: true
|
|
---
|
|
|
|
`
|
|
)
|
|
|
|
// NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
|
|
// in targetPath.
|
|
func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
|
|
cf := hugolib.NewContentFactory(h)
|
|
|
|
if kind == "" {
|
|
kind = cf.SectionFromFilename(targetPath)
|
|
}
|
|
|
|
b := &contentBuilder{
|
|
archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs,
|
|
sourceFs: h.PathSpec.Fs.Source,
|
|
ps: h.PathSpec,
|
|
h: h,
|
|
cf: cf,
|
|
|
|
kind: kind,
|
|
targetPath: targetPath,
|
|
}
|
|
|
|
ext := paths.Ext(targetPath)
|
|
|
|
b.setArcheTypeFilenameToUse(ext)
|
|
|
|
if b.isDir {
|
|
return b.buildDir()
|
|
}
|
|
|
|
if ext == "" {
|
|
return errors.Errorf("failed to resolve %q to a archetype template", targetPath)
|
|
}
|
|
|
|
return b.buildFile()
|
|
|
|
}
|
|
|
|
type contentBuilder struct {
|
|
archeTypeFs afero.Fs
|
|
sourceFs afero.Fs
|
|
|
|
ps *helpers.PathSpec
|
|
h *hugolib.HugoSites
|
|
cf hugolib.ContentFactory
|
|
|
|
// Builder state
|
|
archetypeFilename string
|
|
targetPath string
|
|
kind string
|
|
isDir bool
|
|
dirMap archetypeMap
|
|
}
|
|
|
|
func (b *contentBuilder) buildDir() error {
|
|
// Split the dir into content files and the rest.
|
|
if err := b.mapArcheTypeDir(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var contentTargetFilenames []string
|
|
var baseDir string
|
|
|
|
for _, fi := range b.dirMap.contentFiles {
|
|
targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
|
|
abs, err := b.cf.CreateContentPlaceHolder(targetFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if baseDir == "" {
|
|
baseDir = strings.TrimSuffix(abs, targetFilename)
|
|
}
|
|
|
|
contentTargetFilenames = append(contentTargetFilenames, abs)
|
|
}
|
|
|
|
var contentInclusionFilter *glob.FilenameFilter
|
|
if !b.dirMap.siteUsed {
|
|
// We don't need to build everything.
|
|
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
|
|
for _, cn := range contentTargetFilenames {
|
|
if strings.HasPrefix(cn, filename) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
}
|
|
|
|
if err := b.h.Build(hugolib.BuildCfg{SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, filename := range contentTargetFilenames {
|
|
if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Copy the rest as is.
|
|
for _, f := range b.dirMap.otherFiles {
|
|
meta := f.Meta()
|
|
filename := meta.Path
|
|
|
|
in, err := meta.Open()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to open non-content file")
|
|
}
|
|
|
|
targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename))
|
|
targetDir := filepath.Dir(targetFilename)
|
|
|
|
if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
|
|
return errors.Wrapf(err, "failed to create target directory for %q", targetDir)
|
|
}
|
|
|
|
out, err := b.sourceFs.Create(targetFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(out, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
in.Close()
|
|
out.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *contentBuilder) buildFile() error {
|
|
contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
usesSite, err := b.usesSiteVar(b.archetypeFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var contentInclusionFilter *glob.FilenameFilter
|
|
if !usesSite {
|
|
// We don't need to build everything.
|
|
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
|
|
return strings.HasPrefix(contentPlaceholderAbsFilename, filename)
|
|
})
|
|
}
|
|
|
|
if err := b.h.Build(hugolib.BuildCfg{SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil {
|
|
return err
|
|
}
|
|
|
|
b.h.Log.Infof("Content %q created", contentPlaceholderAbsFilename)
|
|
|
|
return b.openInEditorIfConfigured(contentPlaceholderAbsFilename)
|
|
}
|
|
|
|
func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
|
|
var pathsToCheck []string
|
|
|
|
if b.kind != "" {
|
|
pathsToCheck = append(pathsToCheck, b.kind+ext)
|
|
}
|
|
pathsToCheck = append(pathsToCheck, "default"+ext, "default")
|
|
|
|
for _, p := range pathsToCheck {
|
|
fi, err := b.archeTypeFs.Stat(p)
|
|
if err == nil {
|
|
b.archetypeFilename = p
|
|
b.isDir = fi.IsDir()
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error {
|
|
p := b.h.GetContentPage(contentFilename)
|
|
if p == nil {
|
|
panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
|
|
}
|
|
|
|
f, err := b.sourceFs.Create(contentFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
if archetypeFilename == "" {
|
|
return b.cf.AppplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
|
|
}
|
|
|
|
return b.cf.AppplyArchetypeFilename(f, p, b.kind, archetypeFilename)
|
|
|
|
}
|
|
|
|
func (b *contentBuilder) mapArcheTypeDir() error {
|
|
var m archetypeMap
|
|
|
|
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
fil := fi.(hugofs.FileMetaInfo)
|
|
|
|
if files.IsContentFile(path) {
|
|
m.contentFiles = append(m.contentFiles, fil)
|
|
if !m.siteUsed {
|
|
m.siteUsed, err = b.usesSiteVar(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
m.otherFiles = append(m.otherFiles, fil)
|
|
|
|
return nil
|
|
}
|
|
|
|
walkCfg := hugofs.WalkwayConfig{
|
|
WalkFn: walkFn,
|
|
Fs: b.archeTypeFs,
|
|
Root: b.archetypeFilename,
|
|
}
|
|
|
|
w := hugofs.NewWalkway(walkCfg)
|
|
|
|
if err := w.Walk(); err != nil {
|
|
return errors.Wrapf(err, "failed to walk archetype dir %q", b.archetypeFilename)
|
|
}
|
|
|
|
b.dirMap = m
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
|
|
editor := b.h.Cfg.GetString("newContentEditor")
|
|
if editor == "" {
|
|
return nil
|
|
}
|
|
|
|
b.h.Log.Infof("Editing %q with %q ...\n", filename, editor)
|
|
|
|
cmd, err := hexec.SafeCommand(editor, filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (b *contentBuilder) usesSiteVar(filename string) (bool, error) {
|
|
if filename == "" {
|
|
return false, nil
|
|
}
|
|
bb, err := afero.ReadFile(b.archeTypeFs, filename)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to open archetype file")
|
|
}
|
|
|
|
return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
|
|
|
|
}
|
|
|
|
type archetypeMap struct {
|
|
// These needs to be parsed and executed as Go templates.
|
|
contentFiles []hugofs.FileMetaInfo
|
|
// These are just copied to destination.
|
|
otherFiles []hugofs.FileMetaInfo
|
|
// If the templates needs a fully built site. This can potentially be
|
|
// expensive, so only do when needed.
|
|
siteUsed bool
|
|
}
|