github.com/neohugo/neohugo@v0.123.8/create/content.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package create provides functions to create new content.
    15  package create
    16  
    17  import (
    18  	"bytes"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/neohugo/neohugo/hugofs/glob"
    27  
    28  	"github.com/neohugo/neohugo/common/hexec"
    29  	"github.com/neohugo/neohugo/common/hstrings"
    30  	"github.com/neohugo/neohugo/common/paths"
    31  
    32  	"github.com/neohugo/neohugo/hugofs"
    33  	"github.com/neohugo/neohugo/hugofs/files"
    34  
    35  	"github.com/neohugo/neohugo/helpers"
    36  	"github.com/neohugo/neohugo/hugolib"
    37  	"github.com/spf13/afero"
    38  )
    39  
    40  const (
    41  	// DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
    42  	// and the template we use as a fall back.
    43  	DefaultArchetypeTemplateTemplate = `---
    44  title: "{{ replace .File.ContentBaseName "-" " " | title }}"
    45  date: {{ .Date }}
    46  draft: true
    47  ---
    48  
    49  `
    50  )
    51  
    52  // NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
    53  // in targetPath.
    54  func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error {
    55  	if _, err := h.BaseFs.Content.Fs.Stat(""); err != nil {
    56  		return errors.New("no existing content directory configured for this project")
    57  	}
    58  
    59  	cf := hugolib.NewContentFactory(h)
    60  
    61  	if kind == "" {
    62  		var err error
    63  		kind, err = cf.SectionFromFilename(targetPath)
    64  		if err != nil {
    65  			return err
    66  		}
    67  	}
    68  
    69  	b := &contentBuilder{
    70  		archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs,
    71  		sourceFs:    h.PathSpec.Fs.Source,
    72  		ps:          h.PathSpec,
    73  		h:           h,
    74  		cf:          cf,
    75  
    76  		kind:       kind,
    77  		targetPath: targetPath,
    78  		force:      force,
    79  	}
    80  
    81  	ext := paths.Ext(targetPath)
    82  
    83  	b.setArcheTypeFilenameToUse(ext)
    84  
    85  	withBuildLock := func() (string, error) {
    86  		unlock, err := h.BaseFs.LockBuild()
    87  		if err != nil {
    88  			return "", fmt.Errorf("failed to acquire a build lock: %s", err)
    89  		}
    90  		defer unlock()
    91  
    92  		if b.isDir {
    93  			return "", b.buildDir()
    94  		}
    95  
    96  		if ext == "" {
    97  			return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath)
    98  		}
    99  
   100  		if !files.IsContentFile(b.targetPath) {
   101  			return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
   102  		}
   103  
   104  		return b.buildFile()
   105  	}
   106  
   107  	filename, err := withBuildLock()
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if filename != "" {
   113  		return b.openInEditorIfConfigured(filename)
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  type contentBuilder struct {
   120  	archeTypeFs afero.Fs
   121  	sourceFs    afero.Fs
   122  
   123  	ps *helpers.PathSpec
   124  	h  *hugolib.HugoSites
   125  	cf hugolib.ContentFactory
   126  
   127  	// Builder state
   128  	archetypeFi hugofs.FileMetaInfo
   129  	targetPath  string
   130  	kind        string
   131  	isDir       bool
   132  	dirMap      archetypeMap
   133  	force       bool
   134  }
   135  
   136  func (b *contentBuilder) buildDir() error {
   137  	// Split the dir into content files and the rest.
   138  	if err := b.mapArcheTypeDir(); err != nil {
   139  		return err
   140  	}
   141  
   142  	var contentTargetFilenames []string
   143  	var baseDir string
   144  
   145  	for _, fi := range b.dirMap.contentFiles {
   146  
   147  		targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().PathInfo.Path(), b.archetypeFi.Meta().PathInfo.Path()))
   148  
   149  		// ===> post/my-post/pages/bio.md
   150  		abs, err := b.cf.CreateContentPlaceHolder(targetFilename, b.force)
   151  		if err != nil {
   152  			return err
   153  		}
   154  		if baseDir == "" {
   155  			baseDir = strings.TrimSuffix(abs, targetFilename)
   156  		}
   157  
   158  		contentTargetFilenames = append(contentTargetFilenames, abs)
   159  	}
   160  
   161  	var contentInclusionFilter *glob.FilenameFilter
   162  	if !b.dirMap.siteUsed {
   163  		// We don't need to build everything.
   164  		contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
   165  			filename = strings.TrimPrefix(filename, string(os.PathSeparator))
   166  			for _, cn := range contentTargetFilenames {
   167  				if strings.Contains(cn, filename) {
   168  					return true
   169  				}
   170  			}
   171  			return false
   172  		})
   173  	}
   174  
   175  	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
   176  		return err
   177  	}
   178  
   179  	for i, filename := range contentTargetFilenames {
   180  		if err := b.applyArcheType(filename, b.dirMap.contentFiles[i]); err != nil {
   181  			return err
   182  		}
   183  	}
   184  
   185  	// Copy the rest as is.
   186  	for _, fi := range b.dirMap.otherFiles {
   187  		meta := fi.Meta()
   188  
   189  		in, err := meta.Open()
   190  		if err != nil {
   191  			return fmt.Errorf("failed to open non-content file: %w", err)
   192  		}
   193  		targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(fi.Meta().Filename, b.archetypeFi.Meta().Filename))
   194  		targetDir := filepath.Dir(targetFilename)
   195  
   196  		if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
   197  			return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err)
   198  		}
   199  
   200  		out, err := b.sourceFs.Create(targetFilename)
   201  		if err != nil {
   202  			return err
   203  		}
   204  
   205  		_, err = io.Copy(out, in)
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  		in.Close()
   211  		out.Close()
   212  	}
   213  
   214  	b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath))
   215  
   216  	return nil
   217  }
   218  
   219  func (b *contentBuilder) buildFile() (string, error) {
   220  	contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force)
   221  	if err != nil {
   222  		return "", err
   223  	}
   224  
   225  	usesSite, err := b.usesSiteVar(b.archetypeFi)
   226  	if err != nil {
   227  		return "", err
   228  	}
   229  
   230  	var contentInclusionFilter *glob.FilenameFilter
   231  	if !usesSite {
   232  		// We don't need to build everything.
   233  		contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
   234  			filename = strings.TrimPrefix(filename, string(os.PathSeparator))
   235  			return strings.Contains(contentPlaceholderAbsFilename, filename)
   236  		})
   237  	}
   238  
   239  	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
   240  		return "", err
   241  	}
   242  
   243  	if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFi); err != nil {
   244  		return "", err
   245  	}
   246  
   247  	b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename)
   248  
   249  	return contentPlaceholderAbsFilename, nil
   250  }
   251  
   252  func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
   253  	var pathsToCheck []string
   254  
   255  	if b.kind != "" {
   256  		pathsToCheck = append(pathsToCheck, b.kind+ext)
   257  	}
   258  
   259  	pathsToCheck = append(pathsToCheck, "default"+ext)
   260  
   261  	for _, p := range pathsToCheck {
   262  		fi, err := b.archeTypeFs.Stat(p)
   263  		if err == nil {
   264  			b.archetypeFi = fi.(hugofs.FileMetaInfo)
   265  			b.isDir = fi.IsDir()
   266  			return
   267  		}
   268  	}
   269  }
   270  
   271  func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugofs.FileMetaInfo) error {
   272  	p := b.h.GetContentPage(contentFilename)
   273  	if p == nil {
   274  		panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
   275  	}
   276  
   277  	f, err := b.sourceFs.Create(contentFilename)
   278  	if err != nil {
   279  		return err
   280  	}
   281  	defer f.Close()
   282  
   283  	if archetypeFi == nil {
   284  		return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
   285  	}
   286  
   287  	return b.cf.ApplyArchetypeFi(f, p, b.kind, archetypeFi)
   288  }
   289  
   290  func (b *contentBuilder) mapArcheTypeDir() error {
   291  	var m archetypeMap
   292  
   293  	seen := map[hstrings.Tuple]bool{}
   294  
   295  	walkFn := func(path string, fim hugofs.FileMetaInfo) error {
   296  		if fim.IsDir() {
   297  			return nil
   298  		}
   299  
   300  		pi := fim.Meta().PathInfo
   301  
   302  		if pi.IsContent() {
   303  			pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
   304  			if seen[pathLang] {
   305  				// Duplicate content file, e.g. page.md and page.html.
   306  				// In the regular build, we will filter out the duplicates, but
   307  				// for archetype folders these are ambiguous and we need to
   308  				// fail.
   309  				return fmt.Errorf("duplicate content file found in archetype folder: %q; having both e.g. %s.md and %s.html is ambigous", path, pi.BaseNameNoIdentifier(), pi.BaseNameNoIdentifier())
   310  			}
   311  			seen[pathLang] = true
   312  			m.contentFiles = append(m.contentFiles, fim)
   313  			if !m.siteUsed {
   314  				var err error
   315  				m.siteUsed, err = b.usesSiteVar(fim)
   316  				if err != nil {
   317  					return err
   318  				}
   319  			}
   320  			return nil
   321  		}
   322  
   323  		m.otherFiles = append(m.otherFiles, fim)
   324  
   325  		return nil
   326  	}
   327  
   328  	walkCfg := hugofs.WalkwayConfig{
   329  		WalkFn: walkFn,
   330  		Fs:     b.archeTypeFs,
   331  		Root:   filepath.FromSlash(b.archetypeFi.Meta().PathInfo.Path()),
   332  	}
   333  
   334  	w := hugofs.NewWalkway(walkCfg)
   335  
   336  	if err := w.Walk(); err != nil {
   337  		return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFi.Meta().Filename, err)
   338  	}
   339  
   340  	b.dirMap = m
   341  
   342  	return nil
   343  }
   344  
   345  func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
   346  	editor := b.h.Conf.NewContentEditor()
   347  	if editor == "" {
   348  		return nil
   349  	}
   350  
   351  	editorExec := strings.Fields(editor)[0]
   352  	editorFlags := strings.Fields(editor)[1:]
   353  
   354  	var args []any
   355  	for _, editorFlag := range editorFlags {
   356  		args = append(args, editorFlag)
   357  	}
   358  	args = append(
   359  		args,
   360  		filename,
   361  		hexec.WithStdin(os.Stdin),
   362  		hexec.WithStderr(os.Stderr),
   363  		hexec.WithStdout(os.Stdout),
   364  	)
   365  
   366  	b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec)
   367  
   368  	cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...)
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	return cmd.Run()
   374  }
   375  
   376  func (b *contentBuilder) usesSiteVar(fi hugofs.FileMetaInfo) (bool, error) {
   377  	if fi == nil {
   378  		return false, nil
   379  	}
   380  	f, err := fi.Meta().Open()
   381  	if err != nil {
   382  		return false, err
   383  	}
   384  	defer f.Close()
   385  	bb, err := io.ReadAll(f)
   386  	if err != nil {
   387  		return false, fmt.Errorf("failed to read archetype file: %w", err)
   388  	}
   389  
   390  	return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
   391  }
   392  
   393  type archetypeMap struct {
   394  	// These needs to be parsed and executed as Go templates.
   395  	contentFiles []hugofs.FileMetaInfo
   396  	// These are just copied to destination.
   397  	otherFiles []hugofs.FileMetaInfo
   398  	// If the templates needs a fully built site. This can potentially be
   399  	// expensive, so only do when needed.
   400  	siteUsed bool
   401  }