hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

content.go (9243B)

    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 .Name "-" " " | 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) 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 	}
   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 a 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 
  108 	filename, err := withBuildLock()
  109 	if err != nil {
  110 		return err
  111 	}
  112 
  113 	if filename != "" {
  114 		return b.openInEditorIfConfigured(filename)
  115 	}
  116 
  117 	return nil
  118 
  119 }
  120 
  121 type contentBuilder struct {
  122 	archeTypeFs afero.Fs
  123 	sourceFs    afero.Fs
  124 
  125 	ps *helpers.PathSpec
  126 	h  *hugolib.HugoSites
  127 	cf hugolib.ContentFactory
  128 
  129 	// Builder state
  130 	archetypeFilename string
  131 	targetPath        string
  132 	kind              string
  133 	isDir             bool
  134 	dirMap            archetypeMap
  135 }
  136 
  137 func (b *contentBuilder) buildDir() error {
  138 	// Split the dir into content files and the rest.
  139 	if err := b.mapArcheTypeDir(); err != nil {
  140 		return err
  141 	}
  142 
  143 	var contentTargetFilenames []string
  144 	var baseDir string
  145 
  146 	for _, fi := range b.dirMap.contentFiles {
  147 		targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
  148 		abs, err := b.cf.CreateContentPlaceHolder(targetFilename)
  149 		if err != nil {
  150 			return err
  151 		}
  152 		if baseDir == "" {
  153 			baseDir = strings.TrimSuffix(abs, targetFilename)
  154 		}
  155 
  156 		contentTargetFilenames = append(contentTargetFilenames, abs)
  157 	}
  158 
  159 	var contentInclusionFilter *glob.FilenameFilter
  160 	if !b.dirMap.siteUsed {
  161 		// We don't need to build everything.
  162 		contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
  163 			filename = strings.TrimPrefix(filename, string(os.PathSeparator))
  164 			for _, cn := range contentTargetFilenames {
  165 				if strings.Contains(cn, filename) {
  166 					return true
  167 				}
  168 			}
  169 			return false
  170 		})
  171 
  172 	}
  173 
  174 	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
  175 		return err
  176 	}
  177 
  178 	for i, filename := range contentTargetFilenames {
  179 		if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil {
  180 			return err
  181 		}
  182 	}
  183 
  184 	// Copy the rest as is.
  185 	for _, f := range b.dirMap.otherFiles {
  186 		meta := f.Meta()
  187 		filename := meta.Path
  188 
  189 		in, err := meta.Open()
  190 		if err != nil {
  191 			return fmt.Errorf("failed to open non-content file: %w", err)
  192 		}
  193 
  194 		targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename))
  195 		targetDir := filepath.Dir(targetFilename)
  196 
  197 		if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
  198 			return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err)
  199 		}
  200 
  201 		out, err := b.sourceFs.Create(targetFilename)
  202 		if err != nil {
  203 			return err
  204 		}
  205 
  206 		_, err = io.Copy(out, in)
  207 		if err != nil {
  208 			return err
  209 		}
  210 
  211 		in.Close()
  212 		out.Close()
  213 	}
  214 
  215 	b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath))
  216 
  217 	return nil
  218 }
  219 
  220 func (b *contentBuilder) buildFile() (string, error) {
  221 	contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath)
  222 	if err != nil {
  223 		return "", err
  224 	}
  225 
  226 	usesSite, err := b.usesSiteVar(b.archetypeFilename)
  227 	if err != nil {
  228 		return "", err
  229 	}
  230 
  231 	var contentInclusionFilter *glob.FilenameFilter
  232 	if !usesSite {
  233 		// We don't need to build everything.
  234 		contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
  235 			filename = strings.TrimPrefix(filename, string(os.PathSeparator))
  236 			return strings.Contains(contentPlaceholderAbsFilename, filename)
  237 		})
  238 	}
  239 
  240 	if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
  241 		return "", err
  242 	}
  243 
  244 	if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil {
  245 		return "", err
  246 	}
  247 
  248 	b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename)
  249 
  250 	return contentPlaceholderAbsFilename, nil
  251 }
  252 
  253 func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
  254 	var pathsToCheck []string
  255 
  256 	if b.kind != "" {
  257 		pathsToCheck = append(pathsToCheck, b.kind+ext)
  258 	}
  259 
  260 	pathsToCheck = append(pathsToCheck, "default"+ext)
  261 
  262 	for _, p := range pathsToCheck {
  263 		fi, err := b.archeTypeFs.Stat(p)
  264 		if err == nil {
  265 			b.archetypeFilename = p
  266 			b.isDir = fi.IsDir()
  267 			return
  268 		}
  269 	}
  270 
  271 }
  272 
  273 func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error {
  274 	p := b.h.GetContentPage(contentFilename)
  275 	if p == nil {
  276 		panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
  277 	}
  278 
  279 	f, err := b.sourceFs.Create(contentFilename)
  280 	if err != nil {
  281 		return err
  282 	}
  283 	defer f.Close()
  284 
  285 	if archetypeFilename == "" {
  286 		return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
  287 	}
  288 
  289 	return b.cf.ApplyArchetypeFilename(f, p, b.kind, archetypeFilename)
  290 
  291 }
  292 
  293 func (b *contentBuilder) mapArcheTypeDir() error {
  294 	var m archetypeMap
  295 
  296 	walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
  297 		if err != nil {
  298 			return err
  299 		}
  300 
  301 		if fi.IsDir() {
  302 			return nil
  303 		}
  304 
  305 		fil := fi.(hugofs.FileMetaInfo)
  306 
  307 		if files.IsContentFile(path) {
  308 			m.contentFiles = append(m.contentFiles, fil)
  309 			if !m.siteUsed {
  310 				m.siteUsed, err = b.usesSiteVar(path)
  311 				if err != nil {
  312 					return err
  313 				}
  314 			}
  315 			return nil
  316 		}
  317 
  318 		m.otherFiles = append(m.otherFiles, fil)
  319 
  320 		return nil
  321 	}
  322 
  323 	walkCfg := hugofs.WalkwayConfig{
  324 		WalkFn: walkFn,
  325 		Fs:     b.archeTypeFs,
  326 		Root:   b.archetypeFilename,
  327 	}
  328 
  329 	w := hugofs.NewWalkway(walkCfg)
  330 
  331 	if err := w.Walk(); err != nil {
  332 		return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFilename, err)
  333 	}
  334 
  335 	b.dirMap = m
  336 
  337 	return nil
  338 }
  339 
  340 func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
  341 	editor := b.h.Cfg.GetString("newContentEditor")
  342 	if editor == "" {
  343 		return nil
  344 	}
  345 
  346 	editorExec := strings.Fields(editor)[0]
  347 	editorFlags := strings.Fields(editor)[1:]
  348 
  349 	var args []any
  350 	for _, editorFlag := range editorFlags {
  351 		args = append(args, editorFlag)
  352 	}
  353 	args = append(
  354 		args,
  355 		filename,
  356 		hexec.WithStdin(os.Stdin),
  357 		hexec.WithStderr(os.Stderr),
  358 		hexec.WithStdout(os.Stdout),
  359 	)
  360 
  361 	b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec)
  362 
  363 	cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...)
  364 	if err != nil {
  365 		return err
  366 	}
  367 
  368 	return cmd.Run()
  369 }
  370 
  371 func (b *contentBuilder) usesSiteVar(filename string) (bool, error) {
  372 	if filename == "" {
  373 		return false, nil
  374 	}
  375 	bb, err := afero.ReadFile(b.archeTypeFs, filename)
  376 	if err != nil {
  377 		return false, fmt.Errorf("failed to open archetype file: %w", err)
  378 	}
  379 
  380 	return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
  381 
  382 }
  383 
  384 type archetypeMap struct {
  385 	// These needs to be parsed and executed as Go templates.
  386 	contentFiles []hugofs.FileMetaInfo
  387 	// These are just copied to destination.
  388 	otherFiles []hugofs.FileMetaInfo
  389 	// If the templates needs a fully built site. This can potentially be
  390 	// expensive, so only do when needed.
  391 	siteUsed bool
  392 }