github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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  	"strings"
    22  	"sync"
    23  	"unicode/utf8"
    24  
    25  	"errors"
    26  
    27  	"github.com/gohugoio/hugo/common/text"
    28  	"github.com/gohugoio/hugo/common/types/hstring"
    29  	"github.com/gohugoio/hugo/identity"
    30  	"github.com/gohugoio/hugo/parser/pageparser"
    31  	"github.com/mitchellh/mapstructure"
    32  	"github.com/spf13/cast"
    33  
    34  	"github.com/gohugoio/hugo/markup/converter/hooks"
    35  	"github.com/gohugoio/hugo/markup/highlight/chromalexers"
    36  	"github.com/gohugoio/hugo/markup/tableofcontents"
    37  
    38  	"github.com/gohugoio/hugo/markup/converter"
    39  
    40  	"github.com/gohugoio/hugo/lazy"
    41  
    42  	bp "github.com/gohugoio/hugo/bufferpool"
    43  	"github.com/gohugoio/hugo/tpl"
    44  
    45  	"github.com/gohugoio/hugo/helpers"
    46  	"github.com/gohugoio/hugo/output"
    47  	"github.com/gohugoio/hugo/resources/page"
    48  	"github.com/gohugoio/hugo/resources/resource"
    49  )
    50  
    51  var (
    52  	nopTargetPath    = targetPathsHolder{}
    53  	nopPagePerOutput = struct {
    54  		resource.ResourceLinksProvider
    55  		page.ContentProvider
    56  		page.PageRenderProvider
    57  		page.PaginatorProvider
    58  		page.TableOfContentsProvider
    59  		page.AlternativeOutputFormatsProvider
    60  
    61  		targetPather
    62  	}{
    63  		page.NopPage,
    64  		page.NopPage,
    65  		page.NopPage,
    66  		page.NopPage,
    67  		page.NopPage,
    68  		page.NopPage,
    69  		nopTargetPath,
    70  	}
    71  )
    72  
    73  var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
    74  
    75  func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
    76  	parent := p.init
    77  
    78  	var dependencyTracker identity.Manager
    79  	if p.s.running() {
    80  		dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
    81  	}
    82  
    83  	cp := &pageContentOutput{
    84  		dependencyTracker: dependencyTracker,
    85  		p:                 p,
    86  		f:                 po.f,
    87  		renderHooks:       &renderHooks{},
    88  	}
    89  
    90  	initToC := func(ctx context.Context) (err error) {
    91  		if p.cmap == nil {
    92  			// Nothing to do.
    93  			return nil
    94  		}
    95  
    96  		if err := po.cp.initRenderHooks(); err != nil {
    97  			return err
    98  		}
    99  
   100  		f := po.f
   101  		cp.contentPlaceholders, err = p.shortcodeState.prepareShortcodesForPage(ctx, p, f)
   102  		if err != nil {
   103  			return err
   104  		}
   105  
   106  		var hasVariants bool
   107  		cp.workContent, hasVariants, err = p.contentToRender(ctx, p.source.parsed, p.cmap, cp.contentPlaceholders)
   108  		if err != nil {
   109  			return err
   110  		}
   111  		if hasVariants {
   112  			p.pageOutputTemplateVariationsState.Store(2)
   113  		}
   114  
   115  		isHTML := cp.p.m.markup == "html"
   116  
   117  		if !isHTML {
   118  			createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
   119  				cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
   120  				cp.tableOfContents = tocProvider.TableOfContents()
   121  				cp.tableOfContentsHTML = template.HTML(
   122  					cp.tableOfContents.ToHTML(
   123  						cfg.TableOfContents.StartLevel,
   124  						cfg.TableOfContents.EndLevel,
   125  						cfg.TableOfContents.Ordered,
   126  					),
   127  				)
   128  			}
   129  			// If the converter supports doing the parsing separately, we do that.
   130  			parseResult, ok, err := po.contentRenderer.ParseContent(ctx, cp.workContent)
   131  			if err != nil {
   132  				return err
   133  			}
   134  			if ok {
   135  				// This is Goldmark.
   136  				// Store away the parse result for later use.
   137  				createAndSetToC(parseResult)
   138  				cp.astDoc = parseResult.Doc()
   139  
   140  				return nil
   141  			}
   142  
   143  			// This is Asciidoctor etc.
   144  			r, err := po.contentRenderer.ParseAndRenderContent(ctx, cp.workContent, true)
   145  			if err != nil {
   146  				return err
   147  			}
   148  
   149  			cp.workContent = r.Bytes()
   150  
   151  			if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
   152  				createAndSetToC(tocProvider)
   153  			} else {
   154  				tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
   155  				cp.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents)
   156  				cp.tableOfContents = tableofcontents.Empty
   157  				cp.workContent = tmpContent
   158  			}
   159  		}
   160  
   161  		return nil
   162  
   163  	}
   164  
   165  	initContent := func(ctx context.Context) (err error) {
   166  
   167  		p.s.h.IncrContentRender()
   168  
   169  		if p.cmap == nil {
   170  			// Nothing to do.
   171  			return nil
   172  		}
   173  
   174  		if cp.astDoc != nil {
   175  			// The content is parsed, but not rendered.
   176  			r, ok, err := po.contentRenderer.RenderContent(ctx, cp.workContent, cp.astDoc)
   177  			if err != nil {
   178  				return err
   179  			}
   180  			if !ok {
   181  				return errors.New("invalid state: astDoc is set but RenderContent returned false")
   182  			}
   183  
   184  			cp.workContent = r.Bytes()
   185  		}
   186  
   187  		if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
   188  			// There are one or more replacement tokens to be replaced.
   189  			var hasShortcodeVariants bool
   190  			tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
   191  				if token == tocShortcodePlaceholder {
   192  					// The Page's TableOfContents was accessed in a shortcode.
   193  					if cp.tableOfContentsHTML == "" {
   194  						cp.p.s.initInit(ctx, cp.initToC, cp.p)
   195  					}
   196  					return []byte(cp.tableOfContentsHTML), nil
   197  				}
   198  				renderer, found := cp.contentPlaceholders[token]
   199  				if found {
   200  					repl, more, err := renderer.renderShortcode(ctx)
   201  					if err != nil {
   202  						return nil, err
   203  					}
   204  					hasShortcodeVariants = hasShortcodeVariants || more
   205  					return repl, nil
   206  				}
   207  				// This should never happen.
   208  				return nil, fmt.Errorf("unknown shortcode token %q", token)
   209  			}
   210  
   211  			cp.workContent, err = expandShortcodeTokens(ctx, cp.workContent, tokenHandler)
   212  			if err != nil {
   213  				return err
   214  			}
   215  			if hasShortcodeVariants {
   216  				p.pageOutputTemplateVariationsState.Store(2)
   217  			}
   218  		}
   219  
   220  		if cp.p.source.hasSummaryDivider {
   221  			isHTML := cp.p.m.markup == "html"
   222  			if isHTML {
   223  				src := p.source.parsed.Input()
   224  
   225  				// Use the summary sections as they are provided by the user.
   226  				if p.source.posSummaryEnd != -1 {
   227  					cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
   228  				}
   229  
   230  				if cp.p.source.posBodyStart != -1 {
   231  					cp.workContent = src[cp.p.source.posBodyStart:]
   232  				}
   233  
   234  			} else {
   235  				summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
   236  				if err != nil {
   237  					cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
   238  				} else {
   239  					cp.workContent = content
   240  					cp.summary = helpers.BytesToHTML(summary)
   241  				}
   242  			}
   243  		} else if cp.p.m.summary != "" {
   244  			b, err := po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.p.m.summary), false)
   245  			if err != nil {
   246  				return err
   247  			}
   248  			html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
   249  			cp.summary = helpers.BytesToHTML(html)
   250  		}
   251  
   252  		cp.content = helpers.BytesToHTML(cp.workContent)
   253  
   254  		return nil
   255  	}
   256  
   257  	cp.initToC = parent.Branch(func(ctx context.Context) (any, error) {
   258  		return nil, initToC(ctx)
   259  	})
   260  
   261  	// There may be recursive loops in shortcodes and render hooks.
   262  	cp.initMain = cp.initToC.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (any, error) {
   263  		return nil, initContent(ctx)
   264  	})
   265  
   266  	cp.initPlain = cp.initMain.Branch(func(context.Context) (any, error) {
   267  		cp.plain = tpl.StripHTML(string(cp.content))
   268  		cp.plainWords = strings.Fields(cp.plain)
   269  		cp.setWordCounts(p.m.isCJKLanguage)
   270  
   271  		if err := cp.setAutoSummary(); err != nil {
   272  			return err, nil
   273  		}
   274  
   275  		return nil, nil
   276  	})
   277  
   278  	return cp, nil
   279  }
   280  
   281  type renderHooks struct {
   282  	getRenderer hooks.GetRendererFunc
   283  	init        sync.Once
   284  }
   285  
   286  // pageContentOutput represents the Page content for a given output format.
   287  type pageContentOutput struct {
   288  	f output.Format
   289  
   290  	p *pageState
   291  
   292  	// Lazy load dependencies
   293  	initToC   *lazy.Init
   294  	initMain  *lazy.Init
   295  	initPlain *lazy.Init
   296  
   297  	placeholdersEnabled     bool
   298  	placeholdersEnabledInit sync.Once
   299  
   300  	// Renders Markdown hooks.
   301  	renderHooks *renderHooks
   302  
   303  	workContent       []byte
   304  	dependencyTracker identity.Manager // Set in server mode.
   305  
   306  	// Temporary storage of placeholders mapped to their content.
   307  	// These are shortcodes etc. Some of these will need to be replaced
   308  	// after any markup is rendered, so they share a common prefix.
   309  	contentPlaceholders map[string]shortcodeRenderer
   310  
   311  	// Content sections
   312  	content             template.HTML
   313  	summary             template.HTML
   314  	tableOfContents     *tableofcontents.Fragments
   315  	tableOfContentsHTML template.HTML
   316  	// For Goldmark we split Parse and Render.
   317  	astDoc any
   318  
   319  	truncated bool
   320  
   321  	plainWords     []string
   322  	plain          string
   323  	fuzzyWordCount int
   324  	wordCount      int
   325  	readingTime    int
   326  }
   327  
   328  func (p *pageContentOutput) trackDependency(id identity.Provider) {
   329  	if p.dependencyTracker != nil {
   330  		p.dependencyTracker.Add(id)
   331  	}
   332  
   333  }
   334  
   335  func (p *pageContentOutput) Reset() {
   336  	if p.dependencyTracker != nil {
   337  		p.dependencyTracker.Reset()
   338  	}
   339  	p.initToC.Reset()
   340  	p.initMain.Reset()
   341  	p.initPlain.Reset()
   342  	p.renderHooks = &renderHooks{}
   343  }
   344  
   345  func (p *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
   346  	p.p.s.initInit(ctx, p.initToC, p.p)
   347  	if p.tableOfContents == nil {
   348  		return tableofcontents.Empty
   349  	}
   350  	return p.tableOfContents
   351  }
   352  
   353  func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
   354  	p.p.s.initInit(ctx, p.initToC, p.p)
   355  	return p.tableOfContentsHTML
   356  }
   357  
   358  func (p *pageContentOutput) Content(ctx context.Context) (any, error) {
   359  	p.p.s.initInit(ctx, p.initMain, p.p)
   360  	return p.content, nil
   361  }
   362  
   363  func (p *pageContentOutput) FuzzyWordCount(ctx context.Context) int {
   364  	p.p.s.initInit(ctx, p.initPlain, p.p)
   365  	return p.fuzzyWordCount
   366  }
   367  
   368  func (p *pageContentOutput) Len(ctx context.Context) int {
   369  	p.p.s.initInit(ctx, p.initMain, p.p)
   370  	return len(p.content)
   371  }
   372  
   373  func (p *pageContentOutput) Plain(ctx context.Context) string {
   374  	p.p.s.initInit(ctx, p.initPlain, p.p)
   375  	return p.plain
   376  }
   377  
   378  func (p *pageContentOutput) PlainWords(ctx context.Context) []string {
   379  	p.p.s.initInit(ctx, p.initPlain, p.p)
   380  	return p.plainWords
   381  }
   382  
   383  func (p *pageContentOutput) ReadingTime(ctx context.Context) int {
   384  	p.p.s.initInit(ctx, p.initPlain, p.p)
   385  	return p.readingTime
   386  }
   387  
   388  func (p *pageContentOutput) Summary(ctx context.Context) template.HTML {
   389  	p.p.s.initInit(ctx, p.initMain, p.p)
   390  	if !p.p.source.hasSummaryDivider {
   391  		p.p.s.initInit(ctx, p.initPlain, p.p)
   392  	}
   393  	return p.summary
   394  }
   395  
   396  func (p *pageContentOutput) Truncated(ctx context.Context) bool {
   397  	if p.p.truncated {
   398  		return true
   399  	}
   400  	p.p.s.initInit(ctx, p.initPlain, p.p)
   401  	return p.truncated
   402  }
   403  
   404  func (p *pageContentOutput) WordCount(ctx context.Context) int {
   405  	p.p.s.initInit(ctx, p.initPlain, p.p)
   406  	return p.wordCount
   407  }
   408  
   409  func (p *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
   410  	if len(args) < 1 || len(args) > 2 {
   411  		return "", errors.New("want 1 or 2 arguments")
   412  	}
   413  
   414  	var contentToRender string
   415  	opts := defaultRenderStringOpts
   416  	sidx := 1
   417  
   418  	if len(args) == 1 {
   419  		sidx = 0
   420  	} else {
   421  		m, ok := args[0].(map[string]any)
   422  		if !ok {
   423  			return "", errors.New("first argument must be a map")
   424  		}
   425  
   426  		if err := mapstructure.WeakDecode(m, &opts); err != nil {
   427  			return "", fmt.Errorf("failed to decode options: %w", err)
   428  		}
   429  	}
   430  
   431  	contentToRenderv := args[sidx]
   432  
   433  	if _, ok := contentToRenderv.(hstring.RenderedString); ok {
   434  		// This content is already rendered, this is potentially
   435  		// a infinite recursion.
   436  		return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
   437  	}
   438  
   439  	var err error
   440  	contentToRender, err = cast.ToStringE(contentToRenderv)
   441  	if err != nil {
   442  		return "", err
   443  	}
   444  
   445  	if err = p.initRenderHooks(); err != nil {
   446  		return "", err
   447  	}
   448  
   449  	conv := p.p.getContentConverter()
   450  	if opts.Markup != "" && opts.Markup != p.p.m.markup {
   451  		var err error
   452  		// TODO(bep) consider cache
   453  		conv, err = p.p.m.newContentConverter(p.p, opts.Markup)
   454  		if err != nil {
   455  			return "", p.p.wrapError(err)
   456  		}
   457  	}
   458  
   459  	var rendered []byte
   460  
   461  	if pageparser.HasShortcode(contentToRender) {
   462  		// String contains a shortcode.
   463  		parsed, err := pageparser.ParseMain(strings.NewReader(contentToRender), pageparser.Config{})
   464  		if err != nil {
   465  			return "", err
   466  		}
   467  		pm := &pageContentMap{
   468  			items: make([]any, 0, 20),
   469  		}
   470  		s := newShortcodeHandler(p.p, p.p.s)
   471  
   472  		if err := p.p.mapContentForResult(
   473  			parsed,
   474  			s,
   475  			pm,
   476  			opts.Markup,
   477  			nil,
   478  		); err != nil {
   479  			return "", err
   480  		}
   481  
   482  		placeholders, err := s.prepareShortcodesForPage(ctx, p.p, p.f)
   483  		if err != nil {
   484  			return "", err
   485  		}
   486  
   487  		contentToRender, hasVariants, err := p.p.contentToRender(ctx, parsed, pm, placeholders)
   488  		if err != nil {
   489  			return "", err
   490  		}
   491  		if hasVariants {
   492  			p.p.pageOutputTemplateVariationsState.Store(2)
   493  		}
   494  		b, err := p.renderContentWithConverter(ctx, conv, contentToRender, false)
   495  		if err != nil {
   496  			return "", p.p.wrapError(err)
   497  		}
   498  		rendered = b.Bytes()
   499  
   500  		if pm.hasNonMarkdownShortcode || p.placeholdersEnabled {
   501  			var hasShortcodeVariants bool
   502  
   503  			tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
   504  				if token == tocShortcodePlaceholder {
   505  					// The Page's TableOfContents was accessed in a shortcode.
   506  					if p.tableOfContentsHTML == "" {
   507  						p.p.s.initInit(ctx, p.initToC, p.p)
   508  					}
   509  					return []byte(p.tableOfContentsHTML), nil
   510  				}
   511  				renderer, found := placeholders[token]
   512  				if found {
   513  					repl, more, err := renderer.renderShortcode(ctx)
   514  					if err != nil {
   515  						return nil, err
   516  					}
   517  					hasShortcodeVariants = hasShortcodeVariants || more
   518  					return repl, nil
   519  				}
   520  				// This should not happen.
   521  				return nil, fmt.Errorf("unknown shortcode token %q", token)
   522  			}
   523  
   524  			rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
   525  			if err != nil {
   526  				return "", err
   527  			}
   528  			if hasShortcodeVariants {
   529  				p.p.pageOutputTemplateVariationsState.Store(2)
   530  			}
   531  		}
   532  
   533  		// We need a consolidated view in $page.HasShortcode
   534  		p.p.shortcodeState.transferNames(s)
   535  
   536  	} else {
   537  		c, err := p.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
   538  		if err != nil {
   539  			return "", p.p.wrapError(err)
   540  		}
   541  
   542  		rendered = c.Bytes()
   543  	}
   544  
   545  	if opts.Display == "inline" {
   546  		// We may have to rethink this in the future when we get other
   547  		// renderers.
   548  		rendered = p.p.s.ContentSpec.TrimShortHTML(rendered)
   549  	}
   550  
   551  	return template.HTML(string(rendered)), nil
   552  }
   553  
   554  func (p *pageContentOutput) RenderWithTemplateInfo(ctx context.Context, info tpl.Info, layout ...string) (template.HTML, error) {
   555  	p.p.addDependency(info)
   556  	return p.Render(ctx, layout...)
   557  }
   558  
   559  func (p *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
   560  	templ, found, err := p.p.resolveTemplate(layout...)
   561  	if err != nil {
   562  		return "", p.p.wrapError(err)
   563  	}
   564  
   565  	if !found {
   566  		return "", nil
   567  	}
   568  
   569  	p.p.addDependency(templ.(tpl.Info))
   570  
   571  	// Make sure to send the *pageState and not the *pageContentOutput to the template.
   572  	res, err := executeToString(ctx, p.p.s.Tmpl(), templ, p.p)
   573  	if err != nil {
   574  		return "", p.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
   575  	}
   576  	return template.HTML(res), nil
   577  }
   578  
   579  func (p *pageContentOutput) initRenderHooks() error {
   580  	if p == nil {
   581  		return nil
   582  	}
   583  
   584  	p.renderHooks.init.Do(func() {
   585  		if p.p.pageOutputTemplateVariationsState.Load() == 0 {
   586  			p.p.pageOutputTemplateVariationsState.Store(1)
   587  		}
   588  
   589  		type cacheKey struct {
   590  			tp hooks.RendererType
   591  			id any
   592  			f  output.Format
   593  		}
   594  
   595  		renderCache := make(map[cacheKey]any)
   596  		var renderCacheMu sync.Mutex
   597  
   598  		resolvePosition := func(ctx any) text.Position {
   599  			var offset int
   600  
   601  			switch v := ctx.(type) {
   602  			case hooks.CodeblockContext:
   603  				offset = bytes.Index(p.p.source.parsed.Input(), []byte(v.Inner()))
   604  			}
   605  
   606  			pos := p.p.posFromInput(p.p.source.parsed.Input(), offset)
   607  
   608  			if pos.LineNumber > 0 {
   609  				// Move up to the code fence delimiter.
   610  				// This is in line with how we report on shortcodes.
   611  				pos.LineNumber = pos.LineNumber - 1
   612  			}
   613  
   614  			return pos
   615  		}
   616  
   617  		p.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
   618  			renderCacheMu.Lock()
   619  			defer renderCacheMu.Unlock()
   620  
   621  			key := cacheKey{tp: tp, id: id, f: p.f}
   622  			if r, ok := renderCache[key]; ok {
   623  				return r
   624  			}
   625  
   626  			layoutDescriptor := p.p.getLayoutDescriptor()
   627  			layoutDescriptor.RenderingHook = true
   628  			layoutDescriptor.LayoutOverride = false
   629  			layoutDescriptor.Layout = ""
   630  
   631  			switch tp {
   632  			case hooks.LinkRendererType:
   633  				layoutDescriptor.Kind = "render-link"
   634  			case hooks.ImageRendererType:
   635  				layoutDescriptor.Kind = "render-image"
   636  			case hooks.HeadingRendererType:
   637  				layoutDescriptor.Kind = "render-heading"
   638  			case hooks.CodeBlockRendererType:
   639  				layoutDescriptor.Kind = "render-codeblock"
   640  				if id != nil {
   641  					lang := id.(string)
   642  					lexer := chromalexers.Get(lang)
   643  					if lexer != nil {
   644  						layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
   645  					} else {
   646  						layoutDescriptor.KindVariants = lang
   647  					}
   648  				}
   649  			}
   650  
   651  			getHookTemplate := func(f output.Format) (tpl.Template, bool) {
   652  				templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
   653  				if err != nil {
   654  					panic(err)
   655  				}
   656  				return templ, found
   657  			}
   658  
   659  			templ, found1 := getHookTemplate(p.f)
   660  
   661  			if p.p.reusePageOutputContent() {
   662  				// Check if some of the other output formats would give a different template.
   663  				for _, f := range p.p.s.renderFormats {
   664  					if f.Name == p.f.Name {
   665  						continue
   666  					}
   667  					templ2, found2 := getHookTemplate(f)
   668  					if found2 {
   669  						if !found1 {
   670  							templ = templ2
   671  							found1 = true
   672  							break
   673  						}
   674  
   675  						if templ != templ2 {
   676  							p.p.pageOutputTemplateVariationsState.Store(2)
   677  							break
   678  						}
   679  					}
   680  				}
   681  			}
   682  			if !found1 {
   683  				if tp == hooks.CodeBlockRendererType {
   684  					// No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
   685  					r := p.p.s.ContentSpec.Converters.GetHighlighter()
   686  					renderCache[key] = r
   687  					return r
   688  				}
   689  				return nil
   690  			}
   691  
   692  			r := hookRendererTemplate{
   693  				templateHandler: p.p.s.Tmpl(),
   694  				SearchProvider:  templ.(identity.SearchProvider),
   695  				templ:           templ,
   696  				resolvePosition: resolvePosition,
   697  			}
   698  			renderCache[key] = r
   699  			return r
   700  		}
   701  	})
   702  
   703  	return nil
   704  }
   705  
   706  func (p *pageContentOutput) setAutoSummary() error {
   707  	if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
   708  		return nil
   709  	}
   710  
   711  	var summary string
   712  	var truncated bool
   713  
   714  	if p.p.m.isCJKLanguage {
   715  		summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
   716  	} else {
   717  		summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
   718  	}
   719  	p.summary = template.HTML(summary)
   720  
   721  	p.truncated = truncated
   722  
   723  	return nil
   724  }
   725  
   726  func (cp *pageContentOutput) getContentConverter() (converter.Converter, error) {
   727  	if err := cp.initRenderHooks(); err != nil {
   728  		return nil, err
   729  	}
   730  	return cp.p.getContentConverter(), nil
   731  }
   732  
   733  func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) {
   734  	c, err := cp.getContentConverter()
   735  	if err != nil {
   736  		return nil, err
   737  	}
   738  	return cp.renderContentWithConverter(ctx, c, content, renderTOC)
   739  }
   740  
   741  func (cp *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) {
   742  	c, err := cp.getContentConverter()
   743  	if err != nil {
   744  		return nil, false, err
   745  	}
   746  	p, ok := c.(converter.ParseRenderer)
   747  	if !ok {
   748  		return nil, ok, nil
   749  	}
   750  	rctx := converter.RenderContext{
   751  		Ctx:         ctx,
   752  		Src:         content,
   753  		RenderTOC:   true,
   754  		GetRenderer: cp.renderHooks.getRenderer,
   755  	}
   756  	r, err := p.Parse(rctx)
   757  	return r, ok, err
   758  
   759  }
   760  func (cp *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
   761  	c, err := cp.getContentConverter()
   762  	if err != nil {
   763  		return nil, false, err
   764  	}
   765  	p, ok := c.(converter.ParseRenderer)
   766  	if !ok {
   767  		return nil, ok, nil
   768  	}
   769  	rctx := converter.RenderContext{
   770  		Ctx:         ctx,
   771  		Src:         content,
   772  		RenderTOC:   true,
   773  		GetRenderer: cp.renderHooks.getRenderer,
   774  	}
   775  	r, err := p.Render(rctx, doc)
   776  	if err == nil {
   777  		if ids, ok := r.(identity.IdentitiesProvider); ok {
   778  			for _, v := range ids.GetIdentities() {
   779  				cp.trackDependency(v)
   780  			}
   781  		}
   782  	}
   783  
   784  	return r, ok, err
   785  }
   786  
   787  func (cp *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
   788  	r, err := c.Convert(
   789  		converter.RenderContext{
   790  			Ctx:         ctx,
   791  			Src:         content,
   792  			RenderTOC:   renderTOC,
   793  			GetRenderer: cp.renderHooks.getRenderer,
   794  		})
   795  
   796  	if err == nil {
   797  		if ids, ok := r.(identity.IdentitiesProvider); ok {
   798  			for _, v := range ids.GetIdentities() {
   799  				cp.trackDependency(v)
   800  			}
   801  		}
   802  	}
   803  
   804  	return r, err
   805  }
   806  
   807  func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
   808  	if isCJKLanguage {
   809  		p.wordCount = 0
   810  		for _, word := range p.plainWords {
   811  			runeCount := utf8.RuneCountInString(word)
   812  			if len(word) == runeCount {
   813  				p.wordCount++
   814  			} else {
   815  				p.wordCount += runeCount
   816  			}
   817  		}
   818  	} else {
   819  		p.wordCount = helpers.TotalWords(p.plain)
   820  	}
   821  
   822  	// TODO(bep) is set in a test. Fix that.
   823  	if p.fuzzyWordCount == 0 {
   824  		p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
   825  	}
   826  
   827  	if isCJKLanguage {
   828  		p.readingTime = (p.wordCount + 500) / 501
   829  	} else {
   830  		p.readingTime = (p.wordCount + 212) / 213
   831  	}
   832  }
   833  
   834  // A callback to signal that we have inserted a placeholder into the rendered
   835  // content. This avoids doing extra replacement work.
   836  func (p *pageContentOutput) enablePlaceholders() {
   837  	p.placeholdersEnabledInit.Do(func() {
   838  		p.placeholdersEnabled = true
   839  	})
   840  }
   841  
   842  // these will be shifted out when rendering a given output format.
   843  type pagePerOutputProviders interface {
   844  	targetPather
   845  	page.PaginatorProvider
   846  	resource.ResourceLinksProvider
   847  }
   848  
   849  type targetPather interface {
   850  	targetPaths() page.TargetPaths
   851  }
   852  
   853  type targetPathsHolder struct {
   854  	paths page.TargetPaths
   855  	page.OutputFormat
   856  }
   857  
   858  func (t targetPathsHolder) targetPaths() page.TargetPaths {
   859  	return t.paths
   860  }
   861  
   862  func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
   863  	b := bp.GetBuffer()
   864  	defer bp.PutBuffer(b)
   865  	if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
   866  		return "", err
   867  	}
   868  	return b.String(), nil
   869  }
   870  
   871  func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
   872  	defer func() {
   873  		if r := recover(); r != nil {
   874  			err = fmt.Errorf("summary split failed: %s", r)
   875  		}
   876  	}()
   877  
   878  	startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
   879  
   880  	if startDivider == -1 {
   881  		return
   882  	}
   883  
   884  	startTag := "p"
   885  	switch markup {
   886  	case "asciidocext":
   887  		startTag = "div"
   888  	}
   889  
   890  	// Walk back and forward to the surrounding tags.
   891  	start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
   892  	end := bytes.Index(c[startDivider:], []byte("</"+startTag))
   893  
   894  	if start == -1 {
   895  		start = startDivider
   896  	} else {
   897  		start = startDivider - (startDivider - start)
   898  	}
   899  
   900  	if end == -1 {
   901  		end = startDivider + len(internalSummaryDividerBase)
   902  	} else {
   903  		end = startDivider + end + len(startTag) + 3
   904  	}
   905  
   906  	var addDiv bool
   907  
   908  	switch markup {
   909  	case "rst":
   910  		addDiv = true
   911  	}
   912  
   913  	withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
   914  
   915  	if len(withoutDivider) > 0 {
   916  		summary = bytes.TrimSpace(withoutDivider[:start])
   917  	}
   918  
   919  	if addDiv {
   920  		// For the rst
   921  		summary = append(append([]byte(nil), summary...), []byte("</div>")...)
   922  	}
   923  
   924  	if err != nil {
   925  		return
   926  	}
   927  
   928  	content = bytes.TrimSpace(withoutDivider)
   929  
   930  	return
   931  }