hugo/hugolib/page__per_output.go
2024-06-01 12:04:05 +02:00

714 lines
18 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"strings"
"sync"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
var (
nopTargetPath = targetPathsHolder{}
nopPagePerOutput = struct {
resource.ResourceLinksProvider
page.ContentProvider
page.PageRenderProvider
page.PaginatorProvider
page.TableOfContentsProvider
page.AlternativeOutputFormatsProvider
targetPather
}{
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
nopTargetPath,
}
)
func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
cp := &pageContentOutput{
po: po,
renderHooks: &renderHooks{},
otherOutputs: make(map[uint64]*pageContentOutput),
}
return cp, nil
}
type renderHooks struct {
getRenderer hooks.GetRendererFunc
init sync.Once
}
// pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct {
po *pageOutput
// Other pages involved in rendering of this page,
// typically included with .RenderShortcodes.
otherOutputs map[uint64]*pageContentOutput
contentRenderedVersion uint32 // Incremented on reset.
contentRendered bool // Set on content render.
// Renders Markdown hooks.
renderHooks *renderHooks
}
func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) {
pco.po.p.dependencyManagerOutput.AddIdentity(idp.GetIdentity())
}
func (pco *pageContentOutput) Reset() {
if pco == nil {
return
}
pco.contentRenderedVersion++
pco.contentRendered = false
pco.renderHooks = &renderHooks{}
}
func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents
}
func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
content := pco.po.p.m.content
source, err := content.pi.contentSource(content)
if err != nil {
return "", err
}
ct, err := content.contentToC(ctx, pco)
if err != nil {
return "", err
}
var insertPlaceholders bool
var hasVariants bool
cb := setGetContentCallbackInContext.Get(ctx)
if cb != nil {
insertPlaceholders = true
}
c := make([]byte, 0, len(source)+(len(source)/10))
for _, it := range content.pi.itemsStep2 {
switch v := it.(type) {
case pageparser.Item:
c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
case pageContentReplacement:
// Ignore.
case *shortcode:
if !insertPlaceholders || !v.insertPlaceholder() {
// Insert the rendered shortcode.
renderedShortcode, found := ct.contentPlaceholders[v.placeholder]
if !found {
// This should never happen.
panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
}
b, more, err := renderedShortcode.renderShortcode(ctx)
if err != nil {
return "", fmt.Errorf("failed to render shortcode: %w", err)
}
hasVariants = hasVariants || more
c = append(c, []byte(b)...)
} else {
// Insert the placeholder so we can insert the content after
// markdown processing.
c = append(c, []byte(v.placeholder)...)
}
default:
panic(fmt.Sprintf("unknown item type %T", it))
}
}
if hasVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
}
if cb != nil {
cb(pco, ct)
}
if tpl.Context.IsInGoldmark.Get(ctx) {
// This content will be parsed and rendered by Goldmark.
// Wrap it in a special Hugo markup to assign the correct Page from
// the stack.
return template.HTML(hugocontext.Wrap(c, pco.po.p.pid)), nil
}
return helpers.BytesToHTML(c), nil
}
func (pco *pageContentOutput) Content(ctx context.Context) (any, error) {
r, err := pco.po.p.m.content.contentRendered(ctx, pco)
return r.content, err
}
func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML
}
func (p *pageContentOutput) Len(ctx context.Context) int {
return len(p.mustContentRendered(ctx).content)
}
func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary {
r, err := pco.po.p.m.content.contentRendered(ctx, pco)
if err != nil {
pco.fail(err)
}
return r
}
func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords {
r, err := pco.po.p.m.content.contentPlain(ctx, pco)
if err != nil {
pco.fail(err)
}
return r
}
func (pco *pageContentOutput) fail(err error) {
pco.po.p.s.h.FatalError(pco.po.p.wrapError(err))
}
func (pco *pageContentOutput) Plain(ctx context.Context) string {
return pco.mustContentPlain(ctx).plain
}
func (pco *pageContentOutput) PlainWords(ctx context.Context) []string {
return pco.mustContentPlain(ctx).plainWords
}
func (pco *pageContentOutput) ReadingTime(ctx context.Context) int {
return pco.mustContentPlain(ctx).readingTime
}
func (pco *pageContentOutput) WordCount(ctx context.Context) int {
return pco.mustContentPlain(ctx).wordCount
}
func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int {
return pco.mustContentPlain(ctx).fuzzyWordCount
}
func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML {
return pco.mustContentPlain(ctx).summary
}
func (pco *pageContentOutput) Truncated(ctx context.Context) bool {
return pco.mustContentPlain(ctx).summaryTruncated
}
func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
if len(args) < 1 || len(args) > 2 {
return "", errors.New("want 1 or 2 arguments")
}
var contentToRender string
opts := defaultRenderStringOpts
sidx := 1
if len(args) == 1 {
sidx = 0
} else {
m, ok := args[0].(map[string]any)
if !ok {
return "", errors.New("first argument must be a map")
}
if err := mapstructure.WeakDecode(m, &opts); err != nil {
return "", fmt.Errorf("failed to decode options: %w", err)
}
if opts.Markup != "" {
opts.Markup = markup.ResolveMarkup(opts.Markup)
}
}
contentToRenderv := args[sidx]
if _, ok := contentToRenderv.(hstring.RenderedString); ok {
// This content is already rendered, this is potentially
// a infinite recursion.
return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
}
var err error
contentToRender, err = cast.ToStringE(contentToRenderv)
if err != nil {
return "", err
}
if err = pco.initRenderHooks(); err != nil {
return "", err
}
conv := pco.po.p.getContentConverter()
if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType {
var err error
conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
if err != nil {
return "", pco.po.p.wrapError(err)
}
}
var rendered []byte
parseInfo := &contentParseInfo{
h: pco.po.p.s.h,
pid: pco.po.p.pid,
}
if pageparser.HasShortcode(contentToRender) {
contentToRenderb := []byte(contentToRender)
// String contains a shortcode.
parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
NoFrontMatter: true,
NoSummaryDivider: true,
})
if err != nil {
return "", err
}
s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
return "", err
}
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
if err != nil {
return "", err
}
contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
if err != nil {
return "", err
}
if hasVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
}
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
if err != nil {
return "", pco.po.p.wrapError(err)
}
rendered = b.Bytes()
if parseInfo.hasNonMarkdownShortcode {
var hasShortcodeVariants bool
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
if token == tocShortcodePlaceholder {
toc, err := pco.po.p.m.content.contentToC(ctx, pco)
if err != nil {
return nil, err
}
// The Page's TableOfContents was accessed in a shortcode.
return []byte(toc.tableOfContentsHTML), nil
}
renderer, found := placeholders[token]
if found {
repl, more, err := renderer.renderShortcode(ctx)
if err != nil {
return nil, err
}
hasShortcodeVariants = hasShortcodeVariants || more
return repl, nil
}
// This should not happen.
return nil, fmt.Errorf("unknown shortcode token %q", token)
}
rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
if err != nil {
return "", err
}
if hasShortcodeVariants {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
}
}
// We need a consolidated view in $page.HasShortcode
pco.po.p.m.content.shortcodeState.transferNames(s)
} else {
c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
if err != nil {
return "", pco.po.p.wrapError(err)
}
rendered = c.Bytes()
}
if opts.Display == "inline" {
markup := pco.po.p.m.pageConfig.Content.Markup
if opts.Markup != "" {
markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup)
}
rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup)
}
return template.HTML(string(rendered)), nil
}
func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
if len(layout) == 0 {
return "", errors.New("no layout given")
}
templ, found, err := pco.po.p.resolveTemplate(layout...)
if err != nil {
return "", pco.po.p.wrapError(err)
}
if !found {
return "", nil
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
if err != nil {
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
}
return template.HTML(res), nil
}
func (pco *pageContentOutput) initRenderHooks() error {
if pco == nil {
return nil
}
pco.renderHooks.init.Do(func() {
if pco.po.p.pageOutputTemplateVariationsState.Load() == 0 {
pco.po.p.pageOutputTemplateVariationsState.Store(1)
}
type cacheKey struct {
tp hooks.RendererType
id any
f output.Format
}
renderCache := make(map[cacheKey]any)
var renderCacheMu sync.Mutex
resolvePosition := func(ctx any) text.Position {
source := pco.po.p.m.content.mustSource()
var offset int
switch v := ctx.(type) {
case hooks.CodeblockContext:
offset = bytes.Index(source, []byte(v.Inner()))
}
pos := pco.po.p.posFromInput(source, offset)
if pos.LineNumber > 0 {
// Move up to the code fence delimiter.
// This is in line with how we report on shortcodes.
pos.LineNumber = pos.LineNumber - 1
}
return pos
}
pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
renderCacheMu.Lock()
defer renderCacheMu.Unlock()
key := cacheKey{tp: tp, id: id, f: pco.po.f}
if r, ok := renderCache[key]; ok {
return r
}
layoutDescriptor := pco.po.p.getLayoutDescriptor()
layoutDescriptor.RenderingHook = true
layoutDescriptor.LayoutOverride = false
layoutDescriptor.Layout = ""
switch tp {
case hooks.LinkRendererType:
layoutDescriptor.Kind = "render-link"
case hooks.ImageRendererType:
layoutDescriptor.Kind = "render-image"
case hooks.HeadingRendererType:
layoutDescriptor.Kind = "render-heading"
case hooks.CodeBlockRendererType:
layoutDescriptor.Kind = "render-codeblock"
if id != nil {
lang := id.(string)
lexer := chromalexers.Get(lang)
if lexer != nil {
layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
} else {
layoutDescriptor.KindVariants = lang
}
}
}
getHookTemplate := func(f output.Format) (tpl.Template, bool) {
templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
if err != nil {
panic(err)
}
if found {
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
switch templ.Name() {
case "_default/_markup/render-link.html":
if !renderHookConfig.Link.IsEnableDefault() {
return nil, false
}
case "_default/_markup/render-image.html":
if !renderHookConfig.Image.IsEnableDefault() {
return nil, false
}
}
}
}
return templ, found
}
templ, found1 := getHookTemplate(pco.po.f)
if pco.po.p.reusePageOutputContent() {
// Check if some of the other output formats would give a different template.
for _, f := range pco.po.p.s.renderFormats {
if f.Name == pco.po.f.Name {
continue
}
templ2, found2 := getHookTemplate(f)
if found2 {
if !found1 {
templ = templ2
found1 = true
break
}
if templ != templ2 {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
break
}
}
}
}
if !found1 {
if tp == hooks.CodeBlockRendererType {
// No user provided template for code blocks, so we use the native Go version -- which is also faster.
r := pco.po.p.s.ContentSpec.Converters.GetHighlighter()
renderCache[key] = r
return r
}
return nil
}
r := hookRendererTemplate{
templateHandler: pco.po.p.s.Tmpl(),
templ: templ,
resolvePosition: resolvePosition,
}
renderCache[key] = r
return r
}
})
return nil
}
func (pco *pageContentOutput) getContentConverter() (converter.Converter, error) {
if err := pco.initRenderHooks(); err != nil {
return nil, err
}
return pco.po.p.getContentConverter(), nil
}
func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) {
c, err := cp.getContentConverter()
if err != nil {
return nil, err
}
return cp.renderContentWithConverter(ctx, c, content, renderTOC)
}
func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) {
c, err := pco.getContentConverter()
if err != nil {
return nil, false, err
}
p, ok := c.(converter.ParseRenderer)
if !ok {
return nil, ok, nil
}
rctx := converter.RenderContext{
Ctx: ctx,
Src: content,
RenderTOC: true,
GetRenderer: pco.renderHooks.getRenderer,
}
r, err := p.Parse(rctx)
return r, ok, err
}
func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
c, err := pco.getContentConverter()
if err != nil {
return nil, false, err
}
p, ok := c.(converter.ParseRenderer)
if !ok {
return nil, ok, nil
}
rctx := converter.RenderContext{
Ctx: ctx,
Src: content,
RenderTOC: true,
GetRenderer: pco.renderHooks.getRenderer,
}
r, err := p.Render(rctx, doc)
return r, ok, err
}
func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
r, err := c.Convert(
converter.RenderContext{
Ctx: ctx,
Src: content,
RenderTOC: renderTOC,
GetRenderer: pco.renderHooks.getRenderer,
})
return r, err
}
// these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface {
targetPather
page.PaginatorProvider
resource.ResourceLinksProvider
}
type targetPather interface {
targetPaths() page.TargetPaths
}
type targetPathsHolder struct {
paths page.TargetPaths
page.OutputFormat
}
func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths
}
func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
return "", err
}
return b.String(), nil
}
func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("summary split failed: %s", r)
}
}()
startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
if startDivider == -1 {
return
}
startTag := "p"
switch markup {
case media.DefaultContentTypes.AsciiDoc.SubType:
startTag = "div"
}
// Walk back and forward to the surrounding tags.
start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
end := bytes.Index(c[startDivider:], []byte("</"+startTag))
if start == -1 {
start = startDivider
} else {
start = startDivider - (startDivider - start)
}
if end == -1 {
end = startDivider + len(internalSummaryDividerBase)
} else {
end = startDivider + end + len(startTag) + 3
}
var addDiv bool
switch markup {
case "rst":
addDiv = true
}
withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
if len(withoutDivider) > 0 {
summary = bytes.TrimSpace(withoutDivider[:start])
}
if addDiv {
// For the rst
summary = append(append([]byte(nil), summary...), []byte("</div>")...)
}
if err != nil {
return
}
content = bytes.TrimSpace(withoutDivider)
return
}