github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  	"github.com/mitchellh/mapstructure"
    28  	"github.com/pkg/errors"
    29  	"github.com/spf13/cast"
    30  
    31  	"github.com/gohugoio/hugo/markup/converter/hooks"
    32  
    33  	"github.com/gohugoio/hugo/markup/converter"
    34  
    35  	"github.com/alecthomas/chroma/lexers"
    36  	"github.com/gohugoio/hugo/lazy"
    37  
    38  	bp "github.com/gohugoio/hugo/bufferpool"
    39  	"github.com/gohugoio/hugo/tpl"
    40  
    41  	"github.com/gohugoio/hugo/helpers"
    42  	"github.com/gohugoio/hugo/output"
    43  	"github.com/gohugoio/hugo/resources/page"
    44  	"github.com/gohugoio/hugo/resources/resource"
    45  )
    46  
    47  var (
    48  	nopTargetPath    = targetPathsHolder{}
    49  	nopPagePerOutput = struct {
    50  		resource.ResourceLinksProvider
    51  		page.ContentProvider
    52  		page.PageRenderProvider
    53  		page.PaginatorProvider
    54  		page.TableOfContentsProvider
    55  		page.AlternativeOutputFormatsProvider
    56  
    57  		targetPather
    58  	}{
    59  		page.NopPage,
    60  		page.NopPage,
    61  		page.NopPage,
    62  		page.NopPage,
    63  		page.NopPage,
    64  		page.NopPage,
    65  		nopTargetPath,
    66  	}
    67  )
    68  
    69  var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
    70  
    71  func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
    72  	parent := p.init
    73  
    74  	var dependencyTracker identity.Manager
    75  	if p.s.running() {
    76  		dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
    77  	}
    78  
    79  	cp := &pageContentOutput{
    80  		dependencyTracker: dependencyTracker,
    81  		p:                 p,
    82  		f:                 po.f,
    83  		renderHooks:       &renderHooks{},
    84  	}
    85  
    86  	initContent := func() (err error) {
    87  		p.s.h.IncrContentRender()
    88  
    89  		if p.cmap == nil {
    90  			// Nothing to do.
    91  			return nil
    92  		}
    93  		defer func() {
    94  			// See https://github.com/gohugoio/hugo/issues/6210
    95  			if r := recover(); r != nil {
    96  				err = fmt.Errorf("%s", r)
    97  				p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
    98  			}
    99  		}()
   100  
   101  		if err := po.cp.initRenderHooks(); err != nil {
   102  			return err
   103  		}
   104  
   105  		var hasShortcodeVariants bool
   106  
   107  		f := po.f
   108  		cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
   109  		if err != nil {
   110  			return err
   111  		}
   112  
   113  		if hasShortcodeVariants {
   114  			p.pageOutputTemplateVariationsState.Store(2)
   115  		}
   116  
   117  		cp.workContent = p.contentToRender(cp.contentPlaceholders)
   118  
   119  		isHTML := cp.p.m.markup == "html"
   120  
   121  		if !isHTML {
   122  			r, err := cp.renderContent(cp.workContent, true)
   123  			if err != nil {
   124  				return err
   125  			}
   126  
   127  			cp.workContent = r.Bytes()
   128  
   129  			if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
   130  				cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
   131  				cp.tableOfContents = template.HTML(
   132  					tocProvider.TableOfContents().ToHTML(
   133  						cfg.TableOfContents.StartLevel,
   134  						cfg.TableOfContents.EndLevel,
   135  						cfg.TableOfContents.Ordered,
   136  					),
   137  				)
   138  			} else {
   139  				tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
   140  				cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
   141  				cp.workContent = tmpContent
   142  			}
   143  		}
   144  
   145  		if cp.placeholdersEnabled {
   146  			// ToC was accessed via .Page.TableOfContents in the shortcode,
   147  			// at a time when the ToC wasn't ready.
   148  			cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
   149  		}
   150  
   151  		if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
   152  			// There are one or more replacement tokens to be replaced.
   153  			cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
   154  			if err != nil {
   155  				return err
   156  			}
   157  		}
   158  
   159  		if cp.p.source.hasSummaryDivider {
   160  			if isHTML {
   161  				src := p.source.parsed.Input()
   162  
   163  				// Use the summary sections as they are provided by the user.
   164  				if p.source.posSummaryEnd != -1 {
   165  					cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
   166  				}
   167  
   168  				if cp.p.source.posBodyStart != -1 {
   169  					cp.workContent = src[cp.p.source.posBodyStart:]
   170  				}
   171  
   172  			} else {
   173  				summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
   174  				if err != nil {
   175  					cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
   176  				} else {
   177  					cp.workContent = content
   178  					cp.summary = helpers.BytesToHTML(summary)
   179  				}
   180  			}
   181  		} else if cp.p.m.summary != "" {
   182  			b, err := cp.renderContent([]byte(cp.p.m.summary), false)
   183  			if err != nil {
   184  				return err
   185  			}
   186  			html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
   187  			cp.summary = helpers.BytesToHTML(html)
   188  		}
   189  
   190  		cp.content = helpers.BytesToHTML(cp.workContent)
   191  
   192  		return nil
   193  	}
   194  
   195  	// There may be recursive loops in shortcodes and render hooks.
   196  	cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
   197  		return nil, initContent()
   198  	})
   199  
   200  	cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
   201  		cp.plain = helpers.StripHTML(string(cp.content))
   202  		cp.plainWords = strings.Fields(cp.plain)
   203  		cp.setWordCounts(p.m.isCJKLanguage)
   204  
   205  		if err := cp.setAutoSummary(); err != nil {
   206  			return err, nil
   207  		}
   208  
   209  		return nil, nil
   210  	})
   211  
   212  	return cp, nil
   213  }
   214  
   215  type renderHooks struct {
   216  	getRenderer hooks.GetRendererFunc
   217  	init        sync.Once
   218  }
   219  
   220  // pageContentOutput represents the Page content for a given output format.
   221  type pageContentOutput struct {
   222  	f output.Format
   223  
   224  	p *pageState
   225  
   226  	// Lazy load dependencies
   227  	initMain  *lazy.Init
   228  	initPlain *lazy.Init
   229  
   230  	placeholdersEnabled     bool
   231  	placeholdersEnabledInit sync.Once
   232  
   233  	// Renders Markdown hooks.
   234  	renderHooks *renderHooks
   235  
   236  	workContent       []byte
   237  	dependencyTracker identity.Manager // Set in server mode.
   238  
   239  	// Temporary storage of placeholders mapped to their content.
   240  	// These are shortcodes etc. Some of these will need to be replaced
   241  	// after any markup is rendered, so they share a common prefix.
   242  	contentPlaceholders map[string]string
   243  
   244  	// Content sections
   245  	content         template.HTML
   246  	summary         template.HTML
   247  	tableOfContents template.HTML
   248  
   249  	truncated bool
   250  
   251  	plainWords     []string
   252  	plain          string
   253  	fuzzyWordCount int
   254  	wordCount      int
   255  	readingTime    int
   256  }
   257  
   258  func (p *pageContentOutput) trackDependency(id identity.Provider) {
   259  	if p.dependencyTracker != nil {
   260  		p.dependencyTracker.Add(id)
   261  	}
   262  }
   263  
   264  func (p *pageContentOutput) Reset() {
   265  	if p.dependencyTracker != nil {
   266  		p.dependencyTracker.Reset()
   267  	}
   268  	p.initMain.Reset()
   269  	p.initPlain.Reset()
   270  	p.renderHooks = &renderHooks{}
   271  }
   272  
   273  func (p *pageContentOutput) Content() (interface{}, error) {
   274  	if p.p.s.initInit(p.initMain, p.p) {
   275  		return p.content, nil
   276  	}
   277  	return nil, nil
   278  }
   279  
   280  func (p *pageContentOutput) FuzzyWordCount() int {
   281  	p.p.s.initInit(p.initPlain, p.p)
   282  	return p.fuzzyWordCount
   283  }
   284  
   285  func (p *pageContentOutput) Len() int {
   286  	p.p.s.initInit(p.initMain, p.p)
   287  	return len(p.content)
   288  }
   289  
   290  func (p *pageContentOutput) Plain() string {
   291  	p.p.s.initInit(p.initPlain, p.p)
   292  	return p.plain
   293  }
   294  
   295  func (p *pageContentOutput) PlainWords() []string {
   296  	p.p.s.initInit(p.initPlain, p.p)
   297  	return p.plainWords
   298  }
   299  
   300  func (p *pageContentOutput) ReadingTime() int {
   301  	p.p.s.initInit(p.initPlain, p.p)
   302  	return p.readingTime
   303  }
   304  
   305  func (p *pageContentOutput) Summary() template.HTML {
   306  	p.p.s.initInit(p.initMain, p.p)
   307  	if !p.p.source.hasSummaryDivider {
   308  		p.p.s.initInit(p.initPlain, p.p)
   309  	}
   310  	return p.summary
   311  }
   312  
   313  func (p *pageContentOutput) TableOfContents() template.HTML {
   314  	p.p.s.initInit(p.initMain, p.p)
   315  	return p.tableOfContents
   316  }
   317  
   318  func (p *pageContentOutput) Truncated() bool {
   319  	if p.p.truncated {
   320  		return true
   321  	}
   322  	p.p.s.initInit(p.initPlain, p.p)
   323  	return p.truncated
   324  }
   325  
   326  func (p *pageContentOutput) WordCount() int {
   327  	p.p.s.initInit(p.initPlain, p.p)
   328  	return p.wordCount
   329  }
   330  
   331  func (p *pageContentOutput) RenderString(args ...interface{}) (template.HTML, error) {
   332  	if len(args) < 1 || len(args) > 2 {
   333  		return "", errors.New("want 1 or 2 arguments")
   334  	}
   335  
   336  	var s string
   337  	opts := defaultRenderStringOpts
   338  	sidx := 1
   339  
   340  	if len(args) == 1 {
   341  		sidx = 0
   342  	} else {
   343  		m, ok := args[0].(map[string]interface{})
   344  		if !ok {
   345  			return "", errors.New("first argument must be a map")
   346  		}
   347  
   348  		if err := mapstructure.WeakDecode(m, &opts); err != nil {
   349  			return "", errors.WithMessage(err, "failed to decode options")
   350  		}
   351  	}
   352  
   353  	var err error
   354  	s, err = cast.ToStringE(args[sidx])
   355  	if err != nil {
   356  		return "", err
   357  	}
   358  
   359  	if err = p.initRenderHooks(); err != nil {
   360  		return "", err
   361  	}
   362  
   363  	conv := p.p.getContentConverter()
   364  	if opts.Markup != "" && opts.Markup != p.p.m.markup {
   365  		var err error
   366  		// TODO(bep) consider cache
   367  		conv, err = p.p.m.newContentConverter(p.p, opts.Markup, nil)
   368  		if err != nil {
   369  			return "", p.p.wrapError(err)
   370  		}
   371  	}
   372  
   373  	c, err := p.renderContentWithConverter(conv, []byte(s), false)
   374  	if err != nil {
   375  		return "", p.p.wrapError(err)
   376  	}
   377  
   378  	b := c.Bytes()
   379  
   380  	if opts.Display == "inline" {
   381  		// We may have to rethink this in the future when we get other
   382  		// renderers.
   383  		b = p.p.s.ContentSpec.TrimShortHTML(b)
   384  	}
   385  
   386  	return template.HTML(string(b)), nil
   387  }
   388  
   389  func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
   390  	p.p.addDependency(info)
   391  	return p.Render(layout...)
   392  }
   393  
   394  func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) {
   395  	templ, found, err := p.p.resolveTemplate(layout...)
   396  	if err != nil {
   397  		return "", p.p.wrapError(err)
   398  	}
   399  
   400  	if !found {
   401  		return "", nil
   402  	}
   403  
   404  	p.p.addDependency(templ.(tpl.Info))
   405  
   406  	// Make sure to send the *pageState and not the *pageContentOutput to the template.
   407  	res, err := executeToString(p.p.s.Tmpl(), templ, p.p)
   408  	if err != nil {
   409  		return "", p.p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
   410  	}
   411  	return template.HTML(res), nil
   412  }
   413  
   414  func (p *pageContentOutput) initRenderHooks() error {
   415  	if p == nil {
   416  		return nil
   417  	}
   418  
   419  	p.renderHooks.init.Do(func() {
   420  		if p.p.pageOutputTemplateVariationsState.Load() == 0 {
   421  			p.p.pageOutputTemplateVariationsState.Store(1)
   422  		}
   423  
   424  		type cacheKey struct {
   425  			tp hooks.RendererType
   426  			id interface{}
   427  			f  output.Format
   428  		}
   429  
   430  		renderCache := make(map[cacheKey]interface{})
   431  		var renderCacheMu sync.Mutex
   432  
   433  		p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} {
   434  			renderCacheMu.Lock()
   435  			defer renderCacheMu.Unlock()
   436  
   437  			key := cacheKey{tp: tp, id: id, f: p.f}
   438  			if r, ok := renderCache[key]; ok {
   439  				return r
   440  			}
   441  
   442  			layoutDescriptor := p.p.getLayoutDescriptor()
   443  			layoutDescriptor.RenderingHook = true
   444  			layoutDescriptor.LayoutOverride = false
   445  			layoutDescriptor.Layout = ""
   446  
   447  			switch tp {
   448  			case hooks.LinkRendererType:
   449  				layoutDescriptor.Kind = "render-link"
   450  			case hooks.ImageRendererType:
   451  				layoutDescriptor.Kind = "render-image"
   452  			case hooks.HeadingRendererType:
   453  				layoutDescriptor.Kind = "render-heading"
   454  			case hooks.CodeBlockRendererType:
   455  				layoutDescriptor.Kind = "render-codeblock"
   456  				if id != nil {
   457  					lang := id.(string)
   458  					lexer := lexers.Get(lang)
   459  					if lexer != nil {
   460  						layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
   461  					} else {
   462  						layoutDescriptor.KindVariants = lang
   463  					}
   464  				}
   465  			}
   466  
   467  			getHookTemplate := func(f output.Format) (tpl.Template, bool) {
   468  				templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
   469  				if err != nil {
   470  					panic(err)
   471  				}
   472  				return templ, found
   473  			}
   474  
   475  			templ, found1 := getHookTemplate(p.f)
   476  
   477  			if p.p.reusePageOutputContent() {
   478  				// Check if some of the other output formats would give a different template.
   479  				for _, f := range p.p.s.renderFormats {
   480  					if f.Name == p.f.Name {
   481  						continue
   482  					}
   483  					templ2, found2 := getHookTemplate(f)
   484  					if found2 {
   485  						if !found1 {
   486  							templ = templ2
   487  							found1 = true
   488  							break
   489  						}
   490  
   491  						if templ != templ2 {
   492  							p.p.pageOutputTemplateVariationsState.Store(2)
   493  							break
   494  						}
   495  					}
   496  				}
   497  			}
   498  
   499  			if !found1 {
   500  				if tp == hooks.CodeBlockRendererType {
   501  					// No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
   502  					r := p.p.s.ContentSpec.Converters.GetHighlighter()
   503  					renderCache[key] = r
   504  					return r
   505  				}
   506  				return nil
   507  			}
   508  
   509  			r := hookRendererTemplate{
   510  				templateHandler: p.p.s.Tmpl(),
   511  				SearchProvider:  templ.(identity.SearchProvider),
   512  				templ:           templ,
   513  			}
   514  			renderCache[key] = r
   515  			return r
   516  		}
   517  	})
   518  
   519  	return nil
   520  }
   521  
   522  func (p *pageContentOutput) setAutoSummary() error {
   523  	if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
   524  		return nil
   525  	}
   526  
   527  	var summary string
   528  	var truncated bool
   529  
   530  	if p.p.m.isCJKLanguage {
   531  		summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
   532  	} else {
   533  		summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
   534  	}
   535  	p.summary = template.HTML(summary)
   536  
   537  	p.truncated = truncated
   538  
   539  	return nil
   540  }
   541  
   542  func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
   543  	if err := cp.initRenderHooks(); err != nil {
   544  		return nil, err
   545  	}
   546  	c := cp.p.getContentConverter()
   547  	return cp.renderContentWithConverter(c, content, renderTOC)
   548  }
   549  
   550  func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) {
   551  	r, err := c.Convert(
   552  		converter.RenderContext{
   553  			Src:         content,
   554  			RenderTOC:   renderTOC,
   555  			GetRenderer: cp.renderHooks.getRenderer,
   556  		})
   557  
   558  	if err == nil {
   559  		if ids, ok := r.(identity.IdentitiesProvider); ok {
   560  			for _, v := range ids.GetIdentities() {
   561  				cp.trackDependency(v)
   562  			}
   563  		}
   564  	}
   565  
   566  	return r, err
   567  }
   568  
   569  func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
   570  	if isCJKLanguage {
   571  		p.wordCount = 0
   572  		for _, word := range p.plainWords {
   573  			runeCount := utf8.RuneCountInString(word)
   574  			if len(word) == runeCount {
   575  				p.wordCount++
   576  			} else {
   577  				p.wordCount += runeCount
   578  			}
   579  		}
   580  	} else {
   581  		p.wordCount = helpers.TotalWords(p.plain)
   582  	}
   583  
   584  	// TODO(bep) is set in a test. Fix that.
   585  	if p.fuzzyWordCount == 0 {
   586  		p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
   587  	}
   588  
   589  	if isCJKLanguage {
   590  		p.readingTime = (p.wordCount + 500) / 501
   591  	} else {
   592  		p.readingTime = (p.wordCount + 212) / 213
   593  	}
   594  }
   595  
   596  // A callback to signal that we have inserted a placeholder into the rendered
   597  // content. This avoids doing extra replacement work.
   598  func (p *pageContentOutput) enablePlaceholders() {
   599  	p.placeholdersEnabledInit.Do(func() {
   600  		p.placeholdersEnabled = true
   601  	})
   602  }
   603  
   604  // these will be shifted out when rendering a given output format.
   605  type pagePerOutputProviders interface {
   606  	targetPather
   607  	page.PaginatorProvider
   608  	resource.ResourceLinksProvider
   609  }
   610  
   611  type targetPather interface {
   612  	targetPaths() page.TargetPaths
   613  }
   614  
   615  type targetPathsHolder struct {
   616  	paths page.TargetPaths
   617  	page.OutputFormat
   618  }
   619  
   620  func (t targetPathsHolder) targetPaths() page.TargetPaths {
   621  	return t.paths
   622  }
   623  
   624  func executeToString(h tpl.TemplateHandler, templ tpl.Template, data interface{}) (string, error) {
   625  	b := bp.GetBuffer()
   626  	defer bp.PutBuffer(b)
   627  	if err := h.Execute(templ, b, data); err != nil {
   628  		return "", err
   629  	}
   630  	return b.String(), nil
   631  }
   632  
   633  func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
   634  	defer func() {
   635  		if r := recover(); r != nil {
   636  			err = fmt.Errorf("summary split failed: %s", r)
   637  		}
   638  	}()
   639  
   640  	startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
   641  
   642  	if startDivider == -1 {
   643  		return
   644  	}
   645  
   646  	startTag := "p"
   647  	switch markup {
   648  	case "asciidocext":
   649  		startTag = "div"
   650  	}
   651  
   652  	// Walk back and forward to the surrounding tags.
   653  	start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
   654  	end := bytes.Index(c[startDivider:], []byte("</"+startTag))
   655  
   656  	if start == -1 {
   657  		start = startDivider
   658  	} else {
   659  		start = startDivider - (startDivider - start)
   660  	}
   661  
   662  	if end == -1 {
   663  		end = startDivider + len(internalSummaryDividerBase)
   664  	} else {
   665  		end = startDivider + end + len(startTag) + 3
   666  	}
   667  
   668  	var addDiv bool
   669  
   670  	switch markup {
   671  	case "rst":
   672  		addDiv = true
   673  	}
   674  
   675  	withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
   676  
   677  	if len(withoutDivider) > 0 {
   678  		summary = bytes.TrimSpace(withoutDivider[:start])
   679  	}
   680  
   681  	if addDiv {
   682  		// For the rst
   683  		summary = append(append([]byte(nil), summary...), []byte("</div>")...)
   684  	}
   685  
   686  	if err != nil {
   687  		return
   688  	}
   689  
   690  	content = bytes.TrimSpace(withoutDivider)
   691  
   692  	return
   693  }