github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/gohugoio/hugo/hugofs/glob"
    26  
    27  	"github.com/gohugoio/hugo/common/hexec"
    28  	"github.com/gohugoio/hugo/common/paths"
    29  
    30  	"errors"
    31  
    32  	"github.com/gohugoio/hugo/hugofs/files"
    33  
    34  	"github.com/gohugoio/hugo/hugofs"
    35  
    36  	"github.com/gohugoio/hugo/helpers"
    37  	"github.com/gohugoio/hugo/hugolib"
    38  	"github.com/spf13/afero"
    39  )
    40  
    41  const (
    42  	// DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
    43  	// and the template we use as a fall back.
    44  	DefaultArchetypeTemplateTemplate = `---
    45  title: "{{ replace .File.ContentBaseName "-" " " | title }}"
    46  date: {{ .Date }}
    47  draft: true
    48  ---
    49  
    50  `
    51  )
    52  
    53  // NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
    54  // in targetPath.
    55  func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error {
    56  	if h.BaseFs.Content.Dirs == nil {
    57  		return errors.New("no existing content directory configured for this project")
    58  	}
    59  
    60  	cf := hugolib.NewContentFactory(h)
    61  
    62  	if kind == "" {
    63  		var err error
    64  		kind, err = cf.SectionFromFilename(targetPath)
    65  		if err != nil {
    66  			return err
    67  		}
    68  	}
    69  
    70  	b := &contentBuilder{
    71  		archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs,
    72  		sourceFs:    h.PathSpec.Fs.Source,
    73  		ps:          h.PathSpec,
    74  		h:           h,
    75  		cf:          cf,
    76  
    77  		kind:       kind,
    78  		targetPath: targetPath,
    79  		force:      force,
    80  	}
    81  
    82  	ext := paths.Ext(targetPath)
    83  
    84  	b.setArcheTypeFilenameToUse(ext)
    85  
    86  	withBuildLock := func() (string, error) {
    87  		unlock, err := h.BaseFs.LockBuild()
    88  		if err != nil {
    89  			return "", fmt.Errorf("failed to acquire a build lock: %s", err)
    90  		}
    91  		defer unlock()
    92  
    93  		if b.isDir {
    94  			return "", b.buildDir()
    95  		}
    96  
    97  		if ext == "" {
    98  			return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath)
    99  		}
   100  
   101  		if !files.IsContentFile(b.targetPath) {
   102  			return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
   103  		}
   104  
   105  		return b.buildFile()
   106  
   107  	}
   108  
   109  	filename, err := withBuildLock()
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	if filename != "" {
   115  		return b.openInEditorIfConfigured(filename)
   116  	}
   117  
   118  	return nil
   119  
   120  }
   121  
   122  type contentBuilder struct {
   123  	archeTypeFs afero.Fs
   124  	sourceFs    afero.Fs
   125  
   126  	ps *helpers.PathSpec
   127  	h  *hugolib.HugoSites
   128  	cf hugolib.ContentFactory
   129  
   130  	// Builder state
   131  	archetypeFilename string
   132  	targetPath        string
   133  	kind              string
   134  	isDir             bool
   135  	dirMap            archetypeMap
   136  	force             bool
   137  }
   138  
   139  func (b *contentBuilder) buildDir() error {
   140  	// Split the dir into content files and the rest.
   141  	if err := b.mapArcheTypeDir(); err != nil {
   142  		return err
   143  	}
   144  
   145  	var contentTargetFilenames []string
   146  	var baseDir string
   147  
   148  	for _, fi := range b.dirMap.contentFiles {
   149  		targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
   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  
   176  	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
   177  		return err
   178  	}
   179  
   180  	for i, filename := range contentTargetFilenames {
   181  		if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil {
   182  			return err
   183  		}
   184  	}
   185  
   186  	// Copy the rest as is.
   187  	for _, f := range b.dirMap.otherFiles {
   188  		meta := f.Meta()
   189  		filename := meta.Path
   190  
   191  		in, err := meta.Open()
   192  		if err != nil {
   193  			return fmt.Errorf("failed to open non-content file: %w", err)
   194  		}
   195  
   196  		targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename))
   197  		targetDir := filepath.Dir(targetFilename)
   198  
   199  		if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
   200  			return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err)
   201  		}
   202  
   203  		out, err := b.sourceFs.Create(targetFilename)
   204  		if err != nil {
   205  			return err
   206  		}
   207  
   208  		_, err = io.Copy(out, in)
   209  		if err != nil {
   210  			return err
   211  		}
   212  
   213  		in.Close()
   214  		out.Close()
   215  	}
   216  
   217  	b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath))
   218  
   219  	return nil
   220  }
   221  
   222  func (b *contentBuilder) buildFile() (string, error) {
   223  	contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force)
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  
   228  	usesSite, err := b.usesSiteVar(b.archetypeFilename)
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  
   233  	var contentInclusionFilter *glob.FilenameFilter
   234  	if !usesSite {
   235  		// We don't need to build everything.
   236  		contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
   237  			filename = strings.TrimPrefix(filename, string(os.PathSeparator))
   238  			return strings.Contains(contentPlaceholderAbsFilename, filename)
   239  		})
   240  	}
   241  
   242  	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
   243  		return "", err
   244  	}
   245  
   246  	if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil {
   247  		return "", err
   248  	}
   249  
   250  	b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename)
   251  
   252  	return contentPlaceholderAbsFilename, nil
   253  }
   254  
   255  func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
   256  	var pathsToCheck []string
   257  
   258  	if b.kind != "" {
   259  		pathsToCheck = append(pathsToCheck, b.kind+ext)
   260  	}
   261  
   262  	pathsToCheck = append(pathsToCheck, "default"+ext)
   263  
   264  	for _, p := range pathsToCheck {
   265  		fi, err := b.archeTypeFs.Stat(p)
   266  		if err == nil {
   267  			b.archetypeFilename = p
   268  			b.isDir = fi.IsDir()
   269  			return
   270  		}
   271  	}
   272  
   273  }
   274  
   275  func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error {
   276  	p := b.h.GetContentPage(contentFilename)
   277  	if p == nil {
   278  		panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
   279  	}
   280  
   281  	f, err := b.sourceFs.Create(contentFilename)
   282  	if err != nil {
   283  		return err
   284  	}
   285  	defer f.Close()
   286  
   287  	if archetypeFilename == "" {
   288  		return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
   289  	}
   290  
   291  	return b.cf.ApplyArchetypeFilename(f, p, b.kind, archetypeFilename)
   292  
   293  }
   294  
   295  func (b *contentBuilder) mapArcheTypeDir() error {
   296  	var m archetypeMap
   297  
   298  	walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
   299  		if err != nil {
   300  			return err
   301  		}
   302  
   303  		if fi.IsDir() {
   304  			return nil
   305  		}
   306  
   307  		fil := fi.(hugofs.FileMetaInfo)
   308  
   309  		if files.IsContentFile(path) {
   310  			m.contentFiles = append(m.contentFiles, fil)
   311  			if !m.siteUsed {
   312  				m.siteUsed, err = b.usesSiteVar(path)
   313  				if err != nil {
   314  					return err
   315  				}
   316  			}
   317  			return nil
   318  		}
   319  
   320  		m.otherFiles = append(m.otherFiles, fil)
   321  
   322  		return nil
   323  	}
   324  
   325  	walkCfg := hugofs.WalkwayConfig{
   326  		WalkFn: walkFn,
   327  		Fs:     b.archeTypeFs,
   328  		Root:   b.archetypeFilename,
   329  	}
   330  
   331  	w := hugofs.NewWalkway(walkCfg)
   332  
   333  	if err := w.Walk(); err != nil {
   334  		return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFilename, err)
   335  	}
   336  
   337  	b.dirMap = m
   338  
   339  	return nil
   340  }
   341  
   342  func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
   343  	editor := b.h.Conf.NewContentEditor()
   344  	if editor == "" {
   345  		return nil
   346  	}
   347  
   348  	editorExec := strings.Fields(editor)[0]
   349  	editorFlags := strings.Fields(editor)[1:]
   350  
   351  	var args []any
   352  	for _, editorFlag := range editorFlags {
   353  		args = append(args, editorFlag)
   354  	}
   355  	args = append(
   356  		args,
   357  		filename,
   358  		hexec.WithStdin(os.Stdin),
   359  		hexec.WithStderr(os.Stderr),
   360  		hexec.WithStdout(os.Stdout),
   361  	)
   362  
   363  	b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec)
   364  
   365  	cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...)
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	return cmd.Run()
   371  }
   372  
   373  func (b *contentBuilder) usesSiteVar(filename string) (bool, error) {
   374  	if filename == "" {
   375  		return false, nil
   376  	}
   377  	bb, err := afero.ReadFile(b.archeTypeFs, filename)
   378  	if err != nil {
   379  		return false, fmt.Errorf("failed to open archetype file: %w", err)
   380  	}
   381  
   382  	return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
   383  
   384  }
   385  
   386  type archetypeMap struct {
   387  	// These needs to be parsed and executed as Go templates.
   388  	contentFiles []hugofs.FileMetaInfo
   389  	// These are just copied to destination.
   390  	otherFiles []hugofs.FileMetaInfo
   391  	// If the templates needs a fully built site. This can potentially be
   392  	// expensive, so only do when needed.
   393  	siteUsed bool
   394  }