github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/hugolib/page__per_output.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 hugolib
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"fmt"
    20  	"html/template"
    21  	"runtime/debug"
    22  	"strings"
    23  	"sync"
    24  	"unicode/utf8"
    25  
    26  	"github.com/gohugoio/hugo/identity"
    27  
    28  	"github.com/gohugoio/hugo/markup/converter/hooks"
    29  
    30  	"github.com/gohugoio/hugo/markup/converter"
    31  
    32  	"github.com/gohugoio/hugo/lazy"
    33  
    34  	bp "github.com/gohugoio/hugo/bufferpool"
    35  	"github.com/gohugoio/hugo/tpl"
    36  
    37  	"github.com/gohugoio/hugo/helpers"
    38  	"github.com/gohugoio/hugo/output"
    39  	"github.com/gohugoio/hugo/resources/page"
    40  	"github.com/gohugoio/hugo/resources/resource"
    41  )
    42  
    43  var (
    44  	nopTargetPath    = targetPathsHolder{}
    45  	nopPagePerOutput = struct {
    46  		resource.ResourceLinksProvider
    47  		page.ContentProvider
    48  		page.PageRenderProvider
    49  		page.PaginatorProvider
    50  		page.TableOfContentsProvider
    51  		page.AlternativeOutputFormatsProvider
    52  
    53  		targetPather
    54  	}{
    55  		page.NopPage,
    56  		page.NopPage,
    57  		page.NopPage,
    58  		page.NopPage,
    59  		page.NopPage,
    60  		page.NopPage,
    61  		nopTargetPath,
    62  	}
    63  )
    64  
    65  var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
    66  
    67  func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
    68  	parent := p.init
    69  
    70  	var dependencyTracker identity.Manager
    71  	if p.s.running() {
    72  		dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
    73  	}
    74  
    75  	cp := &pageContentOutput{
    76  		dependencyTracker: dependencyTracker,
    77  		p:                 p,
    78  		f:                 po.f,
    79  		renderHooks:       &renderHooks{},
    80  	}
    81  
    82  	initContent := func() (err error) {
    83  		p.s.h.IncrContentRender()
    84  
    85  		if p.cmap == nil {
    86  			// Nothing to do.
    87  			return nil
    88  		}
    89  		defer func() {
    90  			// See https://github.com/gohugoio/hugo/issues/6210
    91  			if r := recover(); r != nil {
    92  				err = fmt.Errorf("%s", r)
    93  				p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
    94  			}
    95  		}()
    96  
    97  		if err := po.initRenderHooks(); err != nil {
    98  			return err
    99  		}
   100  
   101  		var hasShortcodeVariants bool
   102  
   103  		f := po.f
   104  		cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
   105  		if err != nil {
   106  			return err
   107  		}
   108  
   109  		enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
   110  
   111  		if enableReuse {
   112  			// Reuse this for the other output formats.
   113  			// We may improve on this, but we really want to avoid re-rendering the content
   114  			// to all output formats.
   115  			// The current rule is that if you need output format-aware shortcodes or
   116  			// content rendering hooks, create a output format-specific template, e.g.
   117  			// myshortcode.amp.html.
   118  			cp.enableReuse()
   119  		}
   120  
   121  		cp.workContent = p.contentToRender(cp.contentPlaceholders)
   122  
   123  		isHTML := cp.p.m.markup == "html"
   124  
   125  		if !isHTML {
   126  			r, err := cp.renderContent(cp.workContent, true)
   127  			if err != nil {
   128  				return err
   129  			}
   130  
   131  			cp.workContent = r.Bytes()
   132  
   133  			if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
   134  				cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
   135  				cp.tableOfContents = template.HTML(
   136  					tocProvider.TableOfContents().ToHTML(
   137  						cfg.TableOfContents.StartLevel,
   138  						cfg.TableOfContents.EndLevel,
   139  						cfg.TableOfContents.Ordered,
   140  					),
   141  				)
   142  			} else {
   143  				tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
   144  				cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
   145  				cp.workContent = tmpContent
   146  			}
   147  		}
   148  
   149  		if cp.placeholdersEnabled {
   150  			// ToC was accessed via .Page.TableOfContents in the shortcode,
   151  			// at a time when the ToC wasn't ready.
   152  			cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
   153  		}
   154  
   155  		if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
   156  			// There are one or more replacement tokens to be replaced.
   157  			cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
   158  			if err != nil {
   159  				return err
   160  			}
   161  		}
   162  
   163  		if cp.p.source.hasSummaryDivider {
   164  			if isHTML {
   165  				src := p.source.parsed.Input()
   166  
   167  				// Use the summary sections as they are provided by the user.
   168  				if p.source.posSummaryEnd != -1 {
   169  					cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
   170  				}
   171  
   172  				if cp.p.source.posBodyStart != -1 {
   173  					cp.workContent = src[cp.p.source.posBodyStart:]
   174  				}
   175  
   176  			} else {
   177  				summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
   178  				if err != nil {
   179  					cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
   180  				} else {
   181  					cp.workContent = content
   182  					cp.summary = helpers.BytesToHTML(summary)
   183  				}
   184  			}
   185  		} else if cp.p.m.summary != "" {
   186  			b, err := cp.renderContent([]byte(cp.p.m.summary), false)
   187  			if err != nil {
   188  				return err
   189  			}
   190  			html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
   191  			cp.summary = helpers.BytesToHTML(html)
   192  		}
   193  
   194  		cp.content = helpers.BytesToHTML(cp.workContent)
   195  
   196  		return nil
   197  	}
   198  
   199  	// Recursive loops can only happen in content files with template code (shortcodes etc.)
   200  	// Avoid creating new goroutines if we don't have to.
   201  	needTimeout := p.shortcodeState.hasShortcodes() || cp.renderHooks != nil
   202  
   203  	if needTimeout {
   204  		cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
   205  			return nil, initContent()
   206  		})
   207  	} else {
   208  		cp.initMain = parent.Branch(func() (interface{}, error) {
   209  			return nil, initContent()
   210  		})
   211  	}
   212  
   213  	cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
   214  		cp.plain = helpers.StripHTML(string(cp.content))
   215  		cp.plainWords = strings.Fields(cp.plain)
   216  		cp.setWordCounts(p.m.isCJKLanguage)
   217  
   218  		if err := cp.setAutoSummary(); err != nil {
   219  			return err, nil
   220  		}
   221  
   222  		return nil, nil
   223  	})
   224  
   225  	return cp, nil
   226  }
   227  
   228  type renderHooks struct {
   229  	hooks hooks.Renderers
   230  	init  sync.Once
   231  }
   232  
   233  // pageContentOutput represents the Page content for a given output format.
   234  type pageContentOutput struct {
   235  	f output.Format
   236  
   237  	// If we can reuse this for other output formats.
   238  	reuse     bool
   239  	reuseInit sync.Once
   240  
   241  	p *pageState
   242  
   243  	// Lazy load dependencies
   244  	initMain  *lazy.Init
   245  	initPlain *lazy.Init
   246  
   247  	placeholdersEnabled     bool
   248  	placeholdersEnabledInit sync.Once
   249  
   250  	renderHooks *renderHooks
   251  
   252  	// Set if there are more than one output format variant
   253  	renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
   254  
   255  	// Content state
   256  
   257  	workContent       []byte
   258  	dependencyTracker identity.Manager // Set in server mode.
   259  
   260  	// Temporary storage of placeholders mapped to their content.
   261  	// These are shortcodes etc. Some of these will need to be replaced
   262  	// after any markup is rendered, so they share a common prefix.
   263  	contentPlaceholders map[string]string
   264  
   265  	// Content sections
   266  	content         template.HTML
   267  	summary         template.HTML
   268  	tableOfContents template.HTML
   269  
   270  	truncated bool
   271  
   272  	plainWords     []string
   273  	plain          string
   274  	fuzzyWordCount int
   275  	wordCount      int
   276  	readingTime    int
   277  }
   278  
   279  func (p *pageContentOutput) trackDependency(id identity.Provider) {
   280  	if p.dependencyTracker != nil {
   281  		p.dependencyTracker.Add(id)
   282  	}
   283  }
   284  
   285  func (p *pageContentOutput) Reset() {
   286  	if p.dependencyTracker != nil {
   287  		p.dependencyTracker.Reset()
   288  	}
   289  	p.initMain.Reset()
   290  	p.initPlain.Reset()
   291  	p.renderHooks = &renderHooks{}
   292  }
   293  
   294  func (p *pageContentOutput) Content() (interface{}, error) {
   295  	if p.p.s.initInit(p.initMain, p.p) {
   296  		return p.content, nil
   297  	}
   298  	return nil, nil
   299  }
   300  
   301  func (p *pageContentOutput) FuzzyWordCount() int {
   302  	p.p.s.initInit(p.initPlain, p.p)
   303  	return p.fuzzyWordCount
   304  }
   305  
   306  func (p *pageContentOutput) Len() int {
   307  	p.p.s.initInit(p.initMain, p.p)
   308  	return len(p.content)
   309  }
   310  
   311  func (p *pageContentOutput) Plain() string {
   312  	p.p.s.initInit(p.initPlain, p.p)
   313  	return p.plain
   314  }
   315  
   316  func (p *pageContentOutput) PlainWords() []string {
   317  	p.p.s.initInit(p.initPlain, p.p)
   318  	return p.plainWords
   319  }
   320  
   321  func (p *pageContentOutput) ReadingTime() int {
   322  	p.p.s.initInit(p.initPlain, p.p)
   323  	return p.readingTime
   324  }
   325  
   326  func (p *pageContentOutput) Summary() template.HTML {
   327  	p.p.s.initInit(p.initMain, p.p)
   328  	if !p.p.source.hasSummaryDivider {
   329  		p.p.s.initInit(p.initPlain, p.p)
   330  	}
   331  	return p.summary
   332  }
   333  
   334  func (p *pageContentOutput) TableOfContents() template.HTML {
   335  	p.p.s.initInit(p.initMain, p.p)
   336  	return p.tableOfContents
   337  }
   338  
   339  func (p *pageContentOutput) Truncated() bool {
   340  	if p.p.truncated {
   341  		return true
   342  	}
   343  	p.p.s.initInit(p.initPlain, p.p)
   344  	return p.truncated
   345  }
   346  
   347  func (p *pageContentOutput) WordCount() int {
   348  	p.p.s.initInit(p.initPlain, p.p)
   349  	return p.wordCount
   350  }
   351  
   352  func (p *pageContentOutput) setAutoSummary() error {
   353  	if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
   354  		return nil
   355  	}
   356  
   357  	var summary string
   358  	var truncated bool
   359  
   360  	if p.p.m.isCJKLanguage {
   361  		summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
   362  	} else {
   363  		summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
   364  	}
   365  	p.summary = template.HTML(summary)
   366  
   367  	p.truncated = truncated
   368  
   369  	return nil
   370  }
   371  
   372  func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
   373  	c := cp.p.getContentConverter()
   374  	return cp.renderContentWithConverter(c, content, renderTOC)
   375  }
   376  
   377  func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) {
   378  	r, err := c.Convert(
   379  		converter.RenderContext{
   380  			Src:         content,
   381  			RenderTOC:   renderTOC,
   382  			RenderHooks: cp.renderHooks.hooks,
   383  		})
   384  
   385  	if err == nil {
   386  		if ids, ok := r.(identity.IdentitiesProvider); ok {
   387  			for _, v := range ids.GetIdentities() {
   388  				cp.trackDependency(v)
   389  			}
   390  		}
   391  	}
   392  
   393  	return r, err
   394  }
   395  
   396  func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
   397  	if isCJKLanguage {
   398  		p.wordCount = 0
   399  		for _, word := range p.plainWords {
   400  			runeCount := utf8.RuneCountInString(word)
   401  			if len(word) == runeCount {
   402  				p.wordCount++
   403  			} else {
   404  				p.wordCount += runeCount
   405  			}
   406  		}
   407  	} else {
   408  		p.wordCount = helpers.TotalWords(p.plain)
   409  	}
   410  
   411  	// TODO(bep) is set in a test. Fix that.
   412  	if p.fuzzyWordCount == 0 {
   413  		p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
   414  	}
   415  
   416  	if isCJKLanguage {
   417  		p.readingTime = (p.wordCount + 500) / 501
   418  	} else {
   419  		p.readingTime = (p.wordCount + 212) / 213
   420  	}
   421  }
   422  
   423  // A callback to signal that we have inserted a placeholder into the rendered
   424  // content. This avoids doing extra replacement work.
   425  func (p *pageContentOutput) enablePlaceholders() {
   426  	p.placeholdersEnabledInit.Do(func() {
   427  		p.placeholdersEnabled = true
   428  	})
   429  }
   430  
   431  func (p *pageContentOutput) enableReuse() {
   432  	p.reuseInit.Do(func() {
   433  		p.reuse = true
   434  	})
   435  }
   436  
   437  // these will be shifted out when rendering a given output format.
   438  type pagePerOutputProviders interface {
   439  	targetPather
   440  	page.PaginatorProvider
   441  	resource.ResourceLinksProvider
   442  }
   443  
   444  type targetPather interface {
   445  	targetPaths() page.TargetPaths
   446  }
   447  
   448  type targetPathsHolder struct {
   449  	paths page.TargetPaths
   450  	page.OutputFormat
   451  }
   452  
   453  func (t targetPathsHolder) targetPaths() page.TargetPaths {
   454  	return t.paths
   455  }
   456  
   457  func executeToString(h tpl.TemplateHandler, templ tpl.Template, data interface{}) (string, error) {
   458  	b := bp.GetBuffer()
   459  	defer bp.PutBuffer(b)
   460  	if err := h.Execute(templ, b, data); err != nil {
   461  		return "", err
   462  	}
   463  	return b.String(), nil
   464  }
   465  
   466  func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
   467  	defer func() {
   468  		if r := recover(); r != nil {
   469  			err = fmt.Errorf("summary split failed: %s", r)
   470  		}
   471  	}()
   472  
   473  	startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
   474  
   475  	if startDivider == -1 {
   476  		return
   477  	}
   478  
   479  	startTag := "p"
   480  	switch markup {
   481  	case "asciidocext":
   482  		startTag = "div"
   483  	}
   484  
   485  	// Walk back and forward to the surrounding tags.
   486  	start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
   487  	end := bytes.Index(c[startDivider:], []byte("</"+startTag))
   488  
   489  	if start == -1 {
   490  		start = startDivider
   491  	} else {
   492  		start = startDivider - (startDivider - start)
   493  	}
   494  
   495  	if end == -1 {
   496  		end = startDivider + len(internalSummaryDividerBase)
   497  	} else {
   498  		end = startDivider + end + len(startTag) + 3
   499  	}
   500  
   501  	var addDiv bool
   502  
   503  	switch markup {
   504  	case "rst":
   505  		addDiv = true
   506  	}
   507  
   508  	withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
   509  
   510  	if len(withoutDivider) > 0 {
   511  		summary = bytes.TrimSpace(withoutDivider[:start])
   512  	}
   513  
   514  	if addDiv {
   515  		// For the rst
   516  		summary = append(append([]byte(nil), summary...), []byte("</div>")...)
   517  	}
   518  
   519  	if err != nil {
   520  		return
   521  	}
   522  
   523  	content = bytes.TrimSpace(withoutDivider)
   524  
   525  	return
   526  }