hugo

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

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

pages_capture.go (13084B)

    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 hugolib
   15 
   16 import (
   17 	"context"
   18 	"fmt"
   19 	"os"
   20 	pth "path"
   21 	"path/filepath"
   22 	"reflect"
   23 
   24 	"github.com/gohugoio/hugo/common/maps"
   25 
   26 	"github.com/gohugoio/hugo/parser/pageparser"
   27 
   28 	"github.com/gohugoio/hugo/hugofs/files"
   29 
   30 	"github.com/gohugoio/hugo/source"
   31 
   32 	"github.com/gohugoio/hugo/common/loggers"
   33 	"github.com/gohugoio/hugo/hugofs"
   34 	"github.com/spf13/afero"
   35 )
   36 
   37 const (
   38 	walkIsRootFileMetaKey = "walkIsRootFileMetaKey"
   39 )
   40 
   41 func newPagesCollector(
   42 	sp *source.SourceSpec,
   43 	contentMap *pageMaps,
   44 	logger loggers.Logger,
   45 	contentTracker *contentChangeMap,
   46 	proc pagesCollectorProcessorProvider, filenames ...string) *pagesCollector {
   47 	return &pagesCollector{
   48 		fs:         sp.SourceFs,
   49 		contentMap: contentMap,
   50 		proc:       proc,
   51 		sp:         sp,
   52 		logger:     logger,
   53 		filenames:  filenames,
   54 		tracker:    contentTracker,
   55 	}
   56 }
   57 
   58 type contentDirKey struct {
   59 	dirname  string
   60 	filename string
   61 	tp       bundleDirType
   62 }
   63 
   64 type fileinfoBundle struct {
   65 	header    hugofs.FileMetaInfo
   66 	resources []hugofs.FileMetaInfo
   67 }
   68 
   69 func (b *fileinfoBundle) containsResource(name string) bool {
   70 	for _, r := range b.resources {
   71 		if r.Name() == name {
   72 			return true
   73 		}
   74 	}
   75 
   76 	return false
   77 }
   78 
   79 type pageBundles map[string]*fileinfoBundle
   80 
   81 type pagesCollector struct {
   82 	sp     *source.SourceSpec
   83 	fs     afero.Fs
   84 	logger loggers.Logger
   85 
   86 	contentMap *pageMaps
   87 
   88 	// Ordered list (bundle headers first) used in partial builds.
   89 	filenames []string
   90 
   91 	// Content files tracker used in partial builds.
   92 	tracker *contentChangeMap
   93 
   94 	proc pagesCollectorProcessorProvider
   95 }
   96 
   97 // isCascadingEdit returns whether the dir represents a cascading edit.
   98 // That is, if a front matter cascade section is removed, added or edited.
   99 // If this is the case we must re-evaluate its descendants.
  100 func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) {
  101 	// This is either a section or a taxonomy node. Find it.
  102 	prefix := cleanTreeKey(dir.dirname)
  103 
  104 	section := "/"
  105 	var isCascade bool
  106 
  107 	c.contentMap.walkBranchesPrefix(prefix, func(s string, n *contentNode) bool {
  108 		if n.fi == nil || dir.filename != n.fi.Meta().Filename {
  109 			return false
  110 		}
  111 
  112 		f, err := n.fi.Meta().Open()
  113 		if err != nil {
  114 			// File may have been removed, assume a cascading edit.
  115 			// Some false positives is not too bad.
  116 			isCascade = true
  117 			return true
  118 		}
  119 
  120 		pf, err := pageparser.ParseFrontMatterAndContent(f)
  121 		f.Close()
  122 		if err != nil {
  123 			isCascade = true
  124 			return true
  125 		}
  126 
  127 		if n.p == nil || n.p.bucket == nil {
  128 			return true
  129 		}
  130 
  131 		section = s
  132 
  133 		maps.PrepareParams(pf.FrontMatter)
  134 		cascade1, ok := pf.FrontMatter["cascade"]
  135 		hasCascade := n.p.bucket.cascade != nil && len(n.p.bucket.cascade) > 0
  136 		if !ok {
  137 			isCascade = hasCascade
  138 
  139 			return true
  140 		}
  141 
  142 		if !hasCascade {
  143 			isCascade = true
  144 			return true
  145 		}
  146 
  147 		for _, v := range n.p.bucket.cascade {
  148 			isCascade = !reflect.DeepEqual(cascade1, v)
  149 			if isCascade {
  150 				break
  151 			}
  152 		}
  153 
  154 		return true
  155 	})
  156 
  157 	return isCascade, section
  158 }
  159 
  160 // Collect.
  161 func (c *pagesCollector) Collect() (collectErr error) {
  162 	c.proc.Start(context.Background())
  163 	defer func() {
  164 		err := c.proc.Wait()
  165 		if collectErr == nil {
  166 			collectErr = err
  167 		}
  168 	}()
  169 
  170 	if len(c.filenames) == 0 {
  171 		// Collect everything.
  172 		collectErr = c.collectDir("", false, nil)
  173 	} else {
  174 		for _, pm := range c.contentMap.pmaps {
  175 			pm.cfg.isRebuild = true
  176 		}
  177 		dirs := make(map[contentDirKey]bool)
  178 		for _, filename := range c.filenames {
  179 			dir, btype := c.tracker.resolveAndRemove(filename)
  180 			dirs[contentDirKey{dir, filename, btype}] = true
  181 		}
  182 
  183 		for dir := range dirs {
  184 			for _, pm := range c.contentMap.pmaps {
  185 				pm.s.ResourceSpec.DeleteBySubstring(dir.dirname)
  186 			}
  187 
  188 			switch dir.tp {
  189 			case bundleLeaf:
  190 				collectErr = c.collectDir(dir.dirname, true, nil)
  191 			case bundleBranch:
  192 				isCascading, section := c.isCascadingEdit(dir)
  193 
  194 				if isCascading {
  195 					c.contentMap.deleteSection(section)
  196 				}
  197 				collectErr = c.collectDir(dir.dirname, !isCascading, nil)
  198 			default:
  199 				// We always start from a directory.
  200 				collectErr = c.collectDir(dir.dirname, true, func(fim hugofs.FileMetaInfo) bool {
  201 					return dir.filename == fim.Meta().Filename
  202 				})
  203 			}
  204 
  205 			if collectErr != nil {
  206 				break
  207 			}
  208 		}
  209 
  210 	}
  211 
  212 	return
  213 }
  214 
  215 func (c *pagesCollector) isBundleHeader(fi hugofs.FileMetaInfo) bool {
  216 	class := fi.Meta().Classifier
  217 	return class == files.ContentClassLeaf || class == files.ContentClassBranch
  218 }
  219 
  220 func (c *pagesCollector) getLang(fi hugofs.FileMetaInfo) string {
  221 	lang := fi.Meta().Lang
  222 	if lang != "" {
  223 		return lang
  224 	}
  225 
  226 	return c.sp.DefaultContentLanguage
  227 }
  228 
  229 func (c *pagesCollector) addToBundle(info hugofs.FileMetaInfo, btyp bundleDirType, bundles pageBundles) error {
  230 	getBundle := func(lang string) *fileinfoBundle {
  231 		return bundles[lang]
  232 	}
  233 
  234 	cloneBundle := func(lang string) *fileinfoBundle {
  235 		// Every bundled content file needs a content file header.
  236 		// Use the default content language if found, else just
  237 		// pick one.
  238 		var (
  239 			source *fileinfoBundle
  240 			found  bool
  241 		)
  242 
  243 		source, found = bundles[c.sp.DefaultContentLanguage]
  244 		if !found {
  245 			for _, b := range bundles {
  246 				source = b
  247 				break
  248 			}
  249 		}
  250 
  251 		if source == nil {
  252 			panic(fmt.Sprintf("no source found, %d", len(bundles)))
  253 		}
  254 
  255 		clone := c.cloneFileInfo(source.header)
  256 		clone.Meta().Lang = lang
  257 
  258 		return &fileinfoBundle{
  259 			header: clone,
  260 		}
  261 	}
  262 
  263 	lang := c.getLang(info)
  264 	bundle := getBundle(lang)
  265 	isBundleHeader := c.isBundleHeader(info)
  266 	if bundle != nil && isBundleHeader {
  267 		// index.md file inside a bundle, see issue 6208.
  268 		info.Meta().Classifier = files.ContentClassContent
  269 		isBundleHeader = false
  270 	}
  271 	classifier := info.Meta().Classifier
  272 	isContent := classifier == files.ContentClassContent
  273 	if bundle == nil {
  274 		if isBundleHeader {
  275 			bundle = &fileinfoBundle{header: info}
  276 			bundles[lang] = bundle
  277 		} else {
  278 			if btyp == bundleBranch {
  279 				// No special logic for branch bundles.
  280 				// Every language needs its own _index.md file.
  281 				// Also, we only clone bundle headers for lonesome, bundled,
  282 				// content files.
  283 				return c.handleFiles(info)
  284 			}
  285 
  286 			if isContent {
  287 				bundle = cloneBundle(lang)
  288 				bundles[lang] = bundle
  289 			}
  290 		}
  291 	}
  292 
  293 	if !isBundleHeader && bundle != nil {
  294 		bundle.resources = append(bundle.resources, info)
  295 	}
  296 
  297 	if classifier == files.ContentClassFile {
  298 		translations := info.Meta().Translations
  299 
  300 		for lang, b := range bundles {
  301 			if !stringSliceContains(lang, translations...) && !b.containsResource(info.Name()) {
  302 
  303 				// Clone and add it to the bundle.
  304 				clone := c.cloneFileInfo(info)
  305 				clone.Meta().Lang = lang
  306 				b.resources = append(b.resources, clone)
  307 			}
  308 		}
  309 	}
  310 
  311 	return nil
  312 }
  313 
  314 func (c *pagesCollector) cloneFileInfo(fi hugofs.FileMetaInfo) hugofs.FileMetaInfo {
  315 	return hugofs.NewFileMetaInfo(fi, hugofs.NewFileMeta())
  316 }
  317 
  318 func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func(fim hugofs.FileMetaInfo) bool) error {
  319 	fi, err := c.fs.Stat(dirname)
  320 	if err != nil {
  321 		if os.IsNotExist(err) {
  322 			// May have been deleted.
  323 			return nil
  324 		}
  325 		return err
  326 	}
  327 
  328 	handleDir := func(
  329 		btype bundleDirType,
  330 		dir hugofs.FileMetaInfo,
  331 		path string,
  332 		readdir []hugofs.FileMetaInfo) error {
  333 		if btype > bundleNot && c.tracker != nil {
  334 			c.tracker.add(path, btype)
  335 		}
  336 
  337 		if btype == bundleBranch {
  338 			if err := c.handleBundleBranch(readdir); err != nil {
  339 				return err
  340 			}
  341 			// A branch bundle is only this directory level, so keep walking.
  342 			return nil
  343 		} else if btype == bundleLeaf {
  344 			if err := c.handleBundleLeaf(dir, path, readdir); err != nil {
  345 				return err
  346 			}
  347 
  348 			return nil
  349 		}
  350 
  351 		if err := c.handleFiles(readdir...); err != nil {
  352 			return err
  353 		}
  354 
  355 		return nil
  356 	}
  357 
  358 	filter := func(fim hugofs.FileMetaInfo) bool {
  359 		if fim.Meta().SkipDir {
  360 			return false
  361 		}
  362 
  363 		if c.sp.IgnoreFile(fim.Meta().Filename) {
  364 			return false
  365 		}
  366 
  367 		if inFilter != nil {
  368 			return inFilter(fim)
  369 		}
  370 		return true
  371 	}
  372 
  373 	preHook := func(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) ([]hugofs.FileMetaInfo, error) {
  374 		var btype bundleDirType
  375 
  376 		filtered := readdir[:0]
  377 		for _, fi := range readdir {
  378 			if filter(fi) {
  379 				filtered = append(filtered, fi)
  380 
  381 				if c.tracker != nil {
  382 					// Track symlinks.
  383 					c.tracker.addSymbolicLinkMapping(fi)
  384 				}
  385 			}
  386 		}
  387 		walkRoot := dir.Meta().IsRootFile
  388 		readdir = filtered
  389 
  390 		// We merge language directories, so there can be duplicates, but they
  391 		// will be ordered, most important first.
  392 		var duplicates []int
  393 		seen := make(map[string]bool)
  394 
  395 		for i, fi := range readdir {
  396 
  397 			if fi.IsDir() {
  398 				continue
  399 			}
  400 
  401 			meta := fi.Meta()
  402 			meta.IsRootFile = walkRoot
  403 			class := meta.Classifier
  404 			translationBase := meta.TranslationBaseNameWithExt
  405 			key := pth.Join(meta.Lang, translationBase)
  406 
  407 			if seen[key] {
  408 				duplicates = append(duplicates, i)
  409 				continue
  410 			}
  411 			seen[key] = true
  412 
  413 			var thisBtype bundleDirType
  414 
  415 			switch class {
  416 			case files.ContentClassLeaf:
  417 				thisBtype = bundleLeaf
  418 			case files.ContentClassBranch:
  419 				thisBtype = bundleBranch
  420 			}
  421 
  422 			// Folders with both index.md and _index.md type of files have
  423 			// undefined behaviour and can never work.
  424 			// The branch variant will win because of sort order, but log
  425 			// a warning about it.
  426 			if thisBtype > bundleNot && btype > bundleNot && thisBtype != btype {
  427 				c.logger.Warnf("Content directory %q have both index.* and _index.* files, pick one.", dir.Meta().Filename)
  428 				// Reclassify it so it will be handled as a content file inside the
  429 				// section, which is in line with the <= 0.55 behaviour.
  430 				meta.Classifier = files.ContentClassContent
  431 			} else if thisBtype > bundleNot {
  432 				btype = thisBtype
  433 			}
  434 
  435 		}
  436 
  437 		if len(duplicates) > 0 {
  438 			for i := len(duplicates) - 1; i >= 0; i-- {
  439 				idx := duplicates[i]
  440 				readdir = append(readdir[:idx], readdir[idx+1:]...)
  441 			}
  442 		}
  443 
  444 		err := handleDir(btype, dir, path, readdir)
  445 		if err != nil {
  446 			return nil, err
  447 		}
  448 
  449 		if btype == bundleLeaf || partial {
  450 			return nil, filepath.SkipDir
  451 		}
  452 
  453 		// Keep walking.
  454 		return readdir, nil
  455 	}
  456 
  457 	var postHook hugofs.WalkHook
  458 	if c.tracker != nil {
  459 		postHook = func(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) ([]hugofs.FileMetaInfo, error) {
  460 			if c.tracker == nil {
  461 				// Nothing to do.
  462 				return readdir, nil
  463 			}
  464 
  465 			return readdir, nil
  466 		}
  467 	}
  468 
  469 	wfn := func(path string, info hugofs.FileMetaInfo, err error) error {
  470 		if err != nil {
  471 			return err
  472 		}
  473 
  474 		return nil
  475 	}
  476 
  477 	fim := fi.(hugofs.FileMetaInfo)
  478 	// Make sure the pages in this directory gets re-rendered,
  479 	// even in fast render mode.
  480 	fim.Meta().IsRootFile = true
  481 
  482 	w := hugofs.NewWalkway(hugofs.WalkwayConfig{
  483 		Fs:       c.fs,
  484 		Logger:   c.logger,
  485 		Root:     dirname,
  486 		Info:     fim,
  487 		HookPre:  preHook,
  488 		HookPost: postHook,
  489 		WalkFn:   wfn,
  490 	})
  491 
  492 	return w.Walk()
  493 }
  494 
  495 func (c *pagesCollector) handleBundleBranch(readdir []hugofs.FileMetaInfo) error {
  496 	// Maps bundles to its language.
  497 	bundles := pageBundles{}
  498 
  499 	var contentFiles []hugofs.FileMetaInfo
  500 
  501 	for _, fim := range readdir {
  502 
  503 		if fim.IsDir() {
  504 			continue
  505 		}
  506 
  507 		meta := fim.Meta()
  508 
  509 		switch meta.Classifier {
  510 		case files.ContentClassContent:
  511 			contentFiles = append(contentFiles, fim)
  512 		default:
  513 			if err := c.addToBundle(fim, bundleBranch, bundles); err != nil {
  514 				return err
  515 			}
  516 		}
  517 
  518 	}
  519 
  520 	// Make sure the section is created before its pages.
  521 	if err := c.proc.Process(bundles); err != nil {
  522 		return err
  523 	}
  524 
  525 	return c.handleFiles(contentFiles...)
  526 }
  527 
  528 func (c *pagesCollector) handleBundleLeaf(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) error {
  529 	// Maps bundles to its language.
  530 	bundles := pageBundles{}
  531 
  532 	walk := func(path string, info hugofs.FileMetaInfo, err error) error {
  533 		if err != nil {
  534 			return err
  535 		}
  536 		if info.IsDir() {
  537 			return nil
  538 		}
  539 
  540 		return c.addToBundle(info, bundleLeaf, bundles)
  541 	}
  542 
  543 	// Start a new walker from the given path.
  544 	w := hugofs.NewWalkway(hugofs.WalkwayConfig{
  545 		Root:       path,
  546 		Fs:         c.fs,
  547 		Logger:     c.logger,
  548 		Info:       dir,
  549 		DirEntries: readdir,
  550 		WalkFn:     walk,
  551 	})
  552 
  553 	if err := w.Walk(); err != nil {
  554 		return err
  555 	}
  556 
  557 	return c.proc.Process(bundles)
  558 }
  559 
  560 func (c *pagesCollector) handleFiles(fis ...hugofs.FileMetaInfo) error {
  561 	for _, fi := range fis {
  562 		if fi.IsDir() {
  563 			continue
  564 		}
  565 
  566 		if err := c.proc.Process(fi); err != nil {
  567 			return err
  568 		}
  569 	}
  570 	return nil
  571 }
  572 
  573 func stringSliceContains(k string, values ...string) bool {
  574 	for _, v := range values {
  575 		if k == v {
  576 			return true
  577 		}
  578 	}
  579 	return false
  580 }