hugo

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

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

page__per_output.go (18992B)

    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 	"errors"
   27 
   28 	"github.com/gohugoio/hugo/common/text"
   29 	"github.com/gohugoio/hugo/common/types/hstring"
   30 	"github.com/gohugoio/hugo/identity"
   31 	"github.com/gohugoio/hugo/parser/pageparser"
   32 	"github.com/mitchellh/mapstructure"
   33 	"github.com/spf13/cast"
   34 
   35 	"github.com/gohugoio/hugo/markup/converter/hooks"
   36 
   37 	"github.com/gohugoio/hugo/markup/converter"
   38 
   39 	"github.com/alecthomas/chroma/v2/lexers"
   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 	initContent := func() (err error) {
   91 		p.s.h.IncrContentRender()
   92 
   93 		if p.cmap == nil {
   94 			// Nothing to do.
   95 			return nil
   96 		}
   97 		defer func() {
   98 			// See https://github.com/gohugoio/hugo/issues/6210
   99 			if r := recover(); r != nil {
  100 				err = fmt.Errorf("%s", r)
  101 				p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
  102 			}
  103 		}()
  104 
  105 		if err := po.cp.initRenderHooks(); err != nil {
  106 			return err
  107 		}
  108 
  109 		var hasShortcodeVariants bool
  110 
  111 		f := po.f
  112 		cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
  113 		if err != nil {
  114 			return err
  115 		}
  116 
  117 		if hasShortcodeVariants {
  118 			p.pageOutputTemplateVariationsState.Store(2)
  119 		}
  120 
  121 		cp.workContent = p.contentToRender(p.source.parsed, p.cmap, 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 	// There may be recursive loops in shortcodes and render hooks.
  200 	cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (any, error) {
  201 		return nil, initContent()
  202 	})
  203 
  204 	cp.initPlain = cp.initMain.Branch(func() (any, error) {
  205 		cp.plain = tpl.StripHTML(string(cp.content))
  206 		cp.plainWords = strings.Fields(cp.plain)
  207 		cp.setWordCounts(p.m.isCJKLanguage)
  208 
  209 		if err := cp.setAutoSummary(); err != nil {
  210 			return err, nil
  211 		}
  212 
  213 		return nil, nil
  214 	})
  215 
  216 	return cp, nil
  217 }
  218 
  219 type renderHooks struct {
  220 	getRenderer hooks.GetRendererFunc
  221 	init        sync.Once
  222 }
  223 
  224 // pageContentOutput represents the Page content for a given output format.
  225 type pageContentOutput struct {
  226 	f output.Format
  227 
  228 	p *pageState
  229 
  230 	// Lazy load dependencies
  231 	initMain  *lazy.Init
  232 	initPlain *lazy.Init
  233 
  234 	placeholdersEnabled     bool
  235 	placeholdersEnabledInit sync.Once
  236 
  237 	// Renders Markdown hooks.
  238 	renderHooks *renderHooks
  239 
  240 	workContent       []byte
  241 	dependencyTracker identity.Manager // Set in server mode.
  242 
  243 	// Temporary storage of placeholders mapped to their content.
  244 	// These are shortcodes etc. Some of these will need to be replaced
  245 	// after any markup is rendered, so they share a common prefix.
  246 	contentPlaceholders map[string]string
  247 
  248 	// Content sections
  249 	content         template.HTML
  250 	summary         template.HTML
  251 	tableOfContents template.HTML
  252 
  253 	truncated bool
  254 
  255 	plainWords     []string
  256 	plain          string
  257 	fuzzyWordCount int
  258 	wordCount      int
  259 	readingTime    int
  260 }
  261 
  262 func (p *pageContentOutput) trackDependency(id identity.Provider) {
  263 	if p.dependencyTracker != nil {
  264 		p.dependencyTracker.Add(id)
  265 	}
  266 }
  267 
  268 func (p *pageContentOutput) Reset() {
  269 	if p.dependencyTracker != nil {
  270 		p.dependencyTracker.Reset()
  271 	}
  272 	p.initMain.Reset()
  273 	p.initPlain.Reset()
  274 	p.renderHooks = &renderHooks{}
  275 }
  276 
  277 func (p *pageContentOutput) Content() (any, error) {
  278 	if p.p.s.initInit(p.initMain, p.p) {
  279 		return p.content, nil
  280 	}
  281 	return nil, nil
  282 }
  283 
  284 func (p *pageContentOutput) FuzzyWordCount() int {
  285 	p.p.s.initInit(p.initPlain, p.p)
  286 	return p.fuzzyWordCount
  287 }
  288 
  289 func (p *pageContentOutput) Len() int {
  290 	p.p.s.initInit(p.initMain, p.p)
  291 	return len(p.content)
  292 }
  293 
  294 func (p *pageContentOutput) Plain() string {
  295 	p.p.s.initInit(p.initPlain, p.p)
  296 	return p.plain
  297 }
  298 
  299 func (p *pageContentOutput) PlainWords() []string {
  300 	p.p.s.initInit(p.initPlain, p.p)
  301 	return p.plainWords
  302 }
  303 
  304 func (p *pageContentOutput) ReadingTime() int {
  305 	p.p.s.initInit(p.initPlain, p.p)
  306 	return p.readingTime
  307 }
  308 
  309 func (p *pageContentOutput) Summary() template.HTML {
  310 	p.p.s.initInit(p.initMain, p.p)
  311 	if !p.p.source.hasSummaryDivider {
  312 		p.p.s.initInit(p.initPlain, p.p)
  313 	}
  314 	return p.summary
  315 }
  316 
  317 func (p *pageContentOutput) TableOfContents() template.HTML {
  318 	p.p.s.initInit(p.initMain, p.p)
  319 	return p.tableOfContents
  320 }
  321 
  322 func (p *pageContentOutput) Truncated() bool {
  323 	if p.p.truncated {
  324 		return true
  325 	}
  326 	p.p.s.initInit(p.initPlain, p.p)
  327 	return p.truncated
  328 }
  329 
  330 func (p *pageContentOutput) WordCount() int {
  331 	p.p.s.initInit(p.initPlain, p.p)
  332 	return p.wordCount
  333 }
  334 
  335 func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) {
  336 	if len(args) < 1 || len(args) > 2 {
  337 		return "", errors.New("want 1 or 2 arguments")
  338 	}
  339 
  340 	var contentToRender string
  341 	opts := defaultRenderStringOpts
  342 	sidx := 1
  343 
  344 	if len(args) == 1 {
  345 		sidx = 0
  346 	} else {
  347 		m, ok := args[0].(map[string]any)
  348 		if !ok {
  349 			return "", errors.New("first argument must be a map")
  350 		}
  351 
  352 		if err := mapstructure.WeakDecode(m, &opts); err != nil {
  353 			return "", fmt.Errorf("failed to decode options: %w", err)
  354 		}
  355 	}
  356 
  357 	contentToRenderv := args[sidx]
  358 
  359 	if _, ok := contentToRenderv.(hstring.RenderedString); ok {
  360 		// This content is already rendered, this is potentially
  361 		// a infinite recursion.
  362 		return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
  363 	}
  364 
  365 	var err error
  366 	contentToRender, err = cast.ToStringE(contentToRenderv)
  367 	if err != nil {
  368 		return "", err
  369 	}
  370 
  371 	if err = p.initRenderHooks(); err != nil {
  372 		return "", err
  373 	}
  374 
  375 	conv := p.p.getContentConverter()
  376 	if opts.Markup != "" && opts.Markup != p.p.m.markup {
  377 		var err error
  378 		// TODO(bep) consider cache
  379 		conv, err = p.p.m.newContentConverter(p.p, opts.Markup)
  380 		if err != nil {
  381 			return "", p.p.wrapError(err)
  382 		}
  383 	}
  384 
  385 	var rendered []byte
  386 
  387 	if strings.Contains(contentToRender, "{{") {
  388 		// Probably a shortcode.
  389 		parsed, err := pageparser.ParseMain(strings.NewReader(contentToRender), pageparser.Config{})
  390 		if err != nil {
  391 			return "", err
  392 		}
  393 		pm := &pageContentMap{
  394 			items: make([]any, 0, 20),
  395 		}
  396 		s := newShortcodeHandler(p.p, p.p.s)
  397 
  398 		if err := p.p.mapContentForResult(
  399 			parsed,
  400 			s,
  401 			pm,
  402 			opts.Markup,
  403 			nil,
  404 		); err != nil {
  405 			return "", err
  406 		}
  407 
  408 		placeholders, hasShortcodeVariants, err := s.renderShortcodesForPage(p.p, p.f)
  409 		if err != nil {
  410 			return "", err
  411 		}
  412 
  413 		if hasShortcodeVariants {
  414 			p.p.pageOutputTemplateVariationsState.Store(2)
  415 		}
  416 
  417 		b, err := p.renderContentWithConverter(conv, p.p.contentToRender(parsed, pm, placeholders), false)
  418 		if err != nil {
  419 			return "", p.p.wrapError(err)
  420 		}
  421 		rendered = b.Bytes()
  422 
  423 		if p.placeholdersEnabled {
  424 			// ToC was accessed via .Page.TableOfContents in the shortcode,
  425 			// at a time when the ToC wasn't ready.
  426 			if _, err := p.p.Content(); err != nil {
  427 				return "", err
  428 			}
  429 			placeholders[tocShortcodePlaceholder] = string(p.tableOfContents)
  430 		}
  431 
  432 		if pm.hasNonMarkdownShortcode || p.placeholdersEnabled {
  433 			rendered, err = replaceShortcodeTokens(rendered, placeholders)
  434 			if err != nil {
  435 				return "", err
  436 			}
  437 		}
  438 
  439 		// We need a consolidated view in $page.HasShortcode
  440 		p.p.shortcodeState.transferNames(s)
  441 
  442 	} else {
  443 		c, err := p.renderContentWithConverter(conv, []byte(contentToRender), false)
  444 		if err != nil {
  445 			return "", p.p.wrapError(err)
  446 		}
  447 
  448 		rendered = c.Bytes()
  449 	}
  450 
  451 	if opts.Display == "inline" {
  452 		// We may have to rethink this in the future when we get other
  453 		// renderers.
  454 		rendered = p.p.s.ContentSpec.TrimShortHTML(rendered)
  455 	}
  456 
  457 	return template.HTML(string(rendered)), nil
  458 }
  459 
  460 func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
  461 	p.p.addDependency(info)
  462 	return p.Render(layout...)
  463 }
  464 
  465 func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) {
  466 	templ, found, err := p.p.resolveTemplate(layout...)
  467 	if err != nil {
  468 		return "", p.p.wrapError(err)
  469 	}
  470 
  471 	if !found {
  472 		return "", nil
  473 	}
  474 
  475 	p.p.addDependency(templ.(tpl.Info))
  476 
  477 	// Make sure to send the *pageState and not the *pageContentOutput to the template.
  478 	res, err := executeToString(p.p.s.Tmpl(), templ, p.p)
  479 	if err != nil {
  480 		return "", p.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
  481 	}
  482 	return template.HTML(res), nil
  483 }
  484 
  485 func (p *pageContentOutput) initRenderHooks() error {
  486 	if p == nil {
  487 		return nil
  488 	}
  489 
  490 	p.renderHooks.init.Do(func() {
  491 		if p.p.pageOutputTemplateVariationsState.Load() == 0 {
  492 			p.p.pageOutputTemplateVariationsState.Store(1)
  493 		}
  494 
  495 		type cacheKey struct {
  496 			tp hooks.RendererType
  497 			id any
  498 			f  output.Format
  499 		}
  500 
  501 		renderCache := make(map[cacheKey]any)
  502 		var renderCacheMu sync.Mutex
  503 
  504 		resolvePosition := func(ctx any) text.Position {
  505 			var offset int
  506 
  507 			switch v := ctx.(type) {
  508 			case hooks.CodeblockContext:
  509 				offset = bytes.Index(p.p.source.parsed.Input(), []byte(v.Inner()))
  510 			}
  511 
  512 			pos := p.p.posFromInput(p.p.source.parsed.Input(), offset)
  513 
  514 			if pos.LineNumber > 0 {
  515 				// Move up to the code fence delimiter.
  516 				// This is in line with how we report on shortcodes.
  517 				pos.LineNumber = pos.LineNumber - 1
  518 			}
  519 
  520 			return pos
  521 		}
  522 
  523 		p.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
  524 			renderCacheMu.Lock()
  525 			defer renderCacheMu.Unlock()
  526 
  527 			key := cacheKey{tp: tp, id: id, f: p.f}
  528 			if r, ok := renderCache[key]; ok {
  529 				return r
  530 			}
  531 
  532 			layoutDescriptor := p.p.getLayoutDescriptor()
  533 			layoutDescriptor.RenderingHook = true
  534 			layoutDescriptor.LayoutOverride = false
  535 			layoutDescriptor.Layout = ""
  536 
  537 			switch tp {
  538 			case hooks.LinkRendererType:
  539 				layoutDescriptor.Kind = "render-link"
  540 			case hooks.ImageRendererType:
  541 				layoutDescriptor.Kind = "render-image"
  542 			case hooks.HeadingRendererType:
  543 				layoutDescriptor.Kind = "render-heading"
  544 			case hooks.CodeBlockRendererType:
  545 				layoutDescriptor.Kind = "render-codeblock"
  546 				if id != nil {
  547 					lang := id.(string)
  548 					lexer := lexers.Get(lang)
  549 					if lexer != nil {
  550 						layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
  551 					} else {
  552 						layoutDescriptor.KindVariants = lang
  553 					}
  554 				}
  555 			}
  556 
  557 			getHookTemplate := func(f output.Format) (tpl.Template, bool) {
  558 				templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
  559 				if err != nil {
  560 					panic(err)
  561 				}
  562 				return templ, found
  563 			}
  564 
  565 			templ, found1 := getHookTemplate(p.f)
  566 
  567 			if p.p.reusePageOutputContent() {
  568 				// Check if some of the other output formats would give a different template.
  569 				for _, f := range p.p.s.renderFormats {
  570 					if f.Name == p.f.Name {
  571 						continue
  572 					}
  573 					templ2, found2 := getHookTemplate(f)
  574 					if found2 {
  575 						if !found1 {
  576 							templ = templ2
  577 							found1 = true
  578 							break
  579 						}
  580 
  581 						if templ != templ2 {
  582 							p.p.pageOutputTemplateVariationsState.Store(2)
  583 							break
  584 						}
  585 					}
  586 				}
  587 			}
  588 			if !found1 {
  589 				if tp == hooks.CodeBlockRendererType {
  590 					// No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
  591 					r := p.p.s.ContentSpec.Converters.GetHighlighter()
  592 					renderCache[key] = r
  593 					return r
  594 				}
  595 				return nil
  596 			}
  597 
  598 			r := hookRendererTemplate{
  599 				templateHandler: p.p.s.Tmpl(),
  600 				SearchProvider:  templ.(identity.SearchProvider),
  601 				templ:           templ,
  602 				resolvePosition: resolvePosition,
  603 			}
  604 			renderCache[key] = r
  605 			return r
  606 		}
  607 	})
  608 
  609 	return nil
  610 }
  611 
  612 func (p *pageContentOutput) setAutoSummary() error {
  613 	if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
  614 		return nil
  615 	}
  616 
  617 	var summary string
  618 	var truncated bool
  619 
  620 	if p.p.m.isCJKLanguage {
  621 		summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
  622 	} else {
  623 		summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
  624 	}
  625 	p.summary = template.HTML(summary)
  626 
  627 	p.truncated = truncated
  628 
  629 	return nil
  630 }
  631 
  632 func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
  633 	if err := cp.initRenderHooks(); err != nil {
  634 		return nil, err
  635 	}
  636 	c := cp.p.getContentConverter()
  637 	return cp.renderContentWithConverter(c, content, renderTOC)
  638 }
  639 
  640 func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) {
  641 	r, err := c.Convert(
  642 		converter.RenderContext{
  643 			Src:         content,
  644 			RenderTOC:   renderTOC,
  645 			GetRenderer: cp.renderHooks.getRenderer,
  646 		})
  647 
  648 	if err == nil {
  649 		if ids, ok := r.(identity.IdentitiesProvider); ok {
  650 			for _, v := range ids.GetIdentities() {
  651 				cp.trackDependency(v)
  652 			}
  653 		}
  654 	}
  655 
  656 	return r, err
  657 }
  658 
  659 func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
  660 	if isCJKLanguage {
  661 		p.wordCount = 0
  662 		for _, word := range p.plainWords {
  663 			runeCount := utf8.RuneCountInString(word)
  664 			if len(word) == runeCount {
  665 				p.wordCount++
  666 			} else {
  667 				p.wordCount += runeCount
  668 			}
  669 		}
  670 	} else {
  671 		p.wordCount = helpers.TotalWords(p.plain)
  672 	}
  673 
  674 	// TODO(bep) is set in a test. Fix that.
  675 	if p.fuzzyWordCount == 0 {
  676 		p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
  677 	}
  678 
  679 	if isCJKLanguage {
  680 		p.readingTime = (p.wordCount + 500) / 501
  681 	} else {
  682 		p.readingTime = (p.wordCount + 212) / 213
  683 	}
  684 }
  685 
  686 // A callback to signal that we have inserted a placeholder into the rendered
  687 // content. This avoids doing extra replacement work.
  688 func (p *pageContentOutput) enablePlaceholders() {
  689 	p.placeholdersEnabledInit.Do(func() {
  690 		p.placeholdersEnabled = true
  691 	})
  692 }
  693 
  694 // these will be shifted out when rendering a given output format.
  695 type pagePerOutputProviders interface {
  696 	targetPather
  697 	page.PaginatorProvider
  698 	resource.ResourceLinksProvider
  699 }
  700 
  701 type targetPather interface {
  702 	targetPaths() page.TargetPaths
  703 }
  704 
  705 type targetPathsHolder struct {
  706 	paths page.TargetPaths
  707 	page.OutputFormat
  708 }
  709 
  710 func (t targetPathsHolder) targetPaths() page.TargetPaths {
  711 	return t.paths
  712 }
  713 
  714 func executeToString(h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
  715 	b := bp.GetBuffer()
  716 	defer bp.PutBuffer(b)
  717 	if err := h.Execute(templ, b, data); err != nil {
  718 		return "", err
  719 	}
  720 	return b.String(), nil
  721 }
  722 
  723 func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
  724 	defer func() {
  725 		if r := recover(); r != nil {
  726 			err = fmt.Errorf("summary split failed: %s", r)
  727 		}
  728 	}()
  729 
  730 	startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
  731 
  732 	if startDivider == -1 {
  733 		return
  734 	}
  735 
  736 	startTag := "p"
  737 	switch markup {
  738 	case "asciidocext":
  739 		startTag = "div"
  740 	}
  741 
  742 	// Walk back and forward to the surrounding tags.
  743 	start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
  744 	end := bytes.Index(c[startDivider:], []byte("</"+startTag))
  745 
  746 	if start == -1 {
  747 		start = startDivider
  748 	} else {
  749 		start = startDivider - (startDivider - start)
  750 	}
  751 
  752 	if end == -1 {
  753 		end = startDivider + len(internalSummaryDividerBase)
  754 	} else {
  755 		end = startDivider + end + len(startTag) + 3
  756 	}
  757 
  758 	var addDiv bool
  759 
  760 	switch markup {
  761 	case "rst":
  762 		addDiv = true
  763 	}
  764 
  765 	withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
  766 
  767 	if len(withoutDivider) > 0 {
  768 		summary = bytes.TrimSpace(withoutDivider[:start])
  769 	}
  770 
  771 	if addDiv {
  772 		// For the rst
  773 		summary = append(append([]byte(nil), summary...), []byte("</div>")...)
  774 	}
  775 
  776 	if err != nil {
  777 		return
  778 	}
  779 
  780 	content = bytes.TrimSpace(withoutDivider)
  781 
  782 	return
  783 }