mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-01 04:41:29 -05:00
519f41dbd7
Fixes #12544
833 lines
21 KiB
Go
833 lines
21 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"
|
|
"io"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/bep/logg"
|
|
"github.com/gohugoio/hugo/common/hcontext"
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/common/hugio"
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/identity"
|
|
"github.com/gohugoio/hugo/markup/converter"
|
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
|
"github.com/gohugoio/hugo/parser/metadecoders"
|
|
"github.com/gohugoio/hugo/parser/pageparser"
|
|
"github.com/gohugoio/hugo/resources"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
"github.com/gohugoio/hugo/tpl"
|
|
)
|
|
|
|
const (
|
|
internalSummaryDividerBase = "HUGOMORE42"
|
|
)
|
|
|
|
var (
|
|
internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase)
|
|
internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n")
|
|
)
|
|
|
|
type pageContentReplacement struct {
|
|
val []byte
|
|
|
|
source pageparser.Item
|
|
}
|
|
|
|
func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) {
|
|
var (
|
|
sourceKey string
|
|
openSource hugio.OpenReadSeekCloser
|
|
isFromContentAdapter = m.pageConfig.IsFromContentAdapter
|
|
)
|
|
|
|
if m.f != nil && !isFromContentAdapter {
|
|
sourceKey = filepath.ToSlash(m.f.Filename())
|
|
if !isFromContentAdapter {
|
|
meta := m.f.FileInfo().Meta()
|
|
openSource = func() (hugio.ReadSeekCloser, error) {
|
|
r, err := meta.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
|
|
}
|
|
return r, nil
|
|
}
|
|
}
|
|
} else if isFromContentAdapter {
|
|
openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser()
|
|
}
|
|
|
|
if sourceKey == "" {
|
|
sourceKey = strconv.FormatUint(pid, 10)
|
|
}
|
|
|
|
pi := &contentParseInfo{
|
|
h: h,
|
|
pid: pid,
|
|
sourceKey: sourceKey,
|
|
openSource: openSource,
|
|
}
|
|
|
|
source, err := pi.contentSource(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items, err := pageparser.ParseBytes(
|
|
source,
|
|
pageparser.Config{
|
|
NoFrontMatter: isFromContentAdapter,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pi.itemsStep1 = items
|
|
|
|
if isFromContentAdapter {
|
|
// No front matter.
|
|
return pi, nil
|
|
}
|
|
|
|
if err := pi.mapFrontMatter(source); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pi, nil
|
|
}
|
|
|
|
func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) {
|
|
var filename string
|
|
if m.f != nil {
|
|
filename = m.f.Filename()
|
|
}
|
|
|
|
c := &cachedContent{
|
|
pm: m.s.pageMap,
|
|
StaleInfo: m,
|
|
shortcodeState: newShortcodeHandler(filename, m.s),
|
|
pi: pi,
|
|
enableEmoji: m.s.conf.EnableEmoji,
|
|
}
|
|
|
|
source, err := c.pi.contentSource(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.parseContentFile(source); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
type cachedContent struct {
|
|
pm *pageMap
|
|
|
|
resource.StaleInfo
|
|
|
|
shortcodeState *shortcodeHandler
|
|
|
|
// Parsed content.
|
|
pi *contentParseInfo
|
|
|
|
enableEmoji bool
|
|
}
|
|
|
|
type contentParseInfo struct {
|
|
h *HugoSites
|
|
|
|
pid uint64
|
|
sourceKey string
|
|
|
|
// The source bytes.
|
|
openSource hugio.OpenReadSeekCloser
|
|
|
|
frontMatter map[string]any
|
|
|
|
// Whether the parsed content contains a summary separator.
|
|
hasSummaryDivider bool
|
|
|
|
// Whether there are more content after the summary divider.
|
|
summaryTruncated bool
|
|
|
|
// Returns the position in bytes after any front matter.
|
|
posMainContent int
|
|
|
|
// Indicates whether we must do placeholder replacements.
|
|
hasNonMarkdownShortcode bool
|
|
|
|
// Items from the page parser.
|
|
// These maps directly to the source
|
|
itemsStep1 pageparser.Items
|
|
|
|
// *shortcode, pageContentReplacement or pageparser.Item
|
|
itemsStep2 []any
|
|
}
|
|
|
|
func (p *contentParseInfo) AddBytes(item pageparser.Item) {
|
|
p.itemsStep2 = append(p.itemsStep2, item)
|
|
}
|
|
|
|
func (p *contentParseInfo) AddReplacement(val []byte, source pageparser.Item) {
|
|
p.itemsStep2 = append(p.itemsStep2, pageContentReplacement{val: val, source: source})
|
|
}
|
|
|
|
func (p *contentParseInfo) AddShortcode(s *shortcode) {
|
|
p.itemsStep2 = append(p.itemsStep2, s)
|
|
if s.insertPlaceholder() {
|
|
p.hasNonMarkdownShortcode = true
|
|
}
|
|
}
|
|
|
|
// contentToRenderForItems returns the content to be processed by Goldmark or similar.
|
|
func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) {
|
|
var hasVariants bool
|
|
c := make([]byte, 0, len(source)+(len(source)/10))
|
|
|
|
for _, it := range pi.itemsStep2 {
|
|
switch v := it.(type) {
|
|
case pageparser.Item:
|
|
c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
|
|
case pageContentReplacement:
|
|
c = append(c, v.val...)
|
|
case *shortcode:
|
|
if !v.insertPlaceholder() {
|
|
// Insert the rendered shortcode.
|
|
renderedShortcode, found := renderedShortcodes[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 nil, false, 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))
|
|
}
|
|
}
|
|
|
|
return c, hasVariants, nil
|
|
}
|
|
|
|
func (c *cachedContent) IsZero() bool {
|
|
return len(c.pi.itemsStep2) == 0
|
|
}
|
|
|
|
func (c *cachedContent) parseContentFile(source []byte) error {
|
|
if source == nil || c.pi.openSource == nil {
|
|
return nil
|
|
}
|
|
|
|
return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState)
|
|
}
|
|
|
|
func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error {
|
|
if c.frontMatter != nil {
|
|
return nil
|
|
}
|
|
|
|
f := pageparser.FormatFromFrontMatterType(it.Type)
|
|
var err error
|
|
c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f)
|
|
if err != nil {
|
|
if fe, ok := err.(herrors.FileError); ok {
|
|
pos := fe.Position()
|
|
|
|
// Offset the starting position of front matter.
|
|
offset := iter.LineNumber(source) - 1
|
|
if f == metadecoders.YAML {
|
|
offset -= 1
|
|
}
|
|
pos.LineNumber += offset
|
|
|
|
fe.UpdatePosition(pos)
|
|
fe.SetFilename("") // It will be set later.
|
|
|
|
return fe
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error {
|
|
if fe, ok := err.(herrors.FileError); ok {
|
|
return fe
|
|
}
|
|
|
|
pos := posFromInput("", source, i.Pos())
|
|
|
|
return herrors.NewFileErrorFromPos(err, pos)
|
|
}
|
|
|
|
func (rn *contentParseInfo) mapFrontMatter(source []byte) error {
|
|
if len(rn.itemsStep1) == 0 {
|
|
return nil
|
|
}
|
|
iter := pageparser.NewIterator(rn.itemsStep1)
|
|
|
|
Loop:
|
|
for {
|
|
it := iter.Next()
|
|
switch {
|
|
case it.IsFrontMatter():
|
|
if err := rn.parseFrontMatter(it, iter, source); err != nil {
|
|
return err
|
|
}
|
|
next := iter.Peek()
|
|
if !next.IsDone() {
|
|
rn.posMainContent = next.Pos()
|
|
}
|
|
// Done.
|
|
break Loop
|
|
case it.IsEOF():
|
|
break Loop
|
|
case it.IsError():
|
|
return rn.failMap(source, it.Err, it)
|
|
default:
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rn *contentParseInfo) mapItemsAfterFrontMatter(
|
|
source []byte,
|
|
s *shortcodeHandler,
|
|
) error {
|
|
if len(rn.itemsStep1) == 0 {
|
|
return nil
|
|
}
|
|
|
|
fail := func(err error, i pageparser.Item) error {
|
|
if fe, ok := err.(herrors.FileError); ok {
|
|
return fe
|
|
}
|
|
|
|
pos := posFromInput("", source, i.Pos())
|
|
|
|
return herrors.NewFileErrorFromPos(err, pos)
|
|
}
|
|
|
|
iter := pageparser.NewIterator(rn.itemsStep1)
|
|
|
|
// the parser is guaranteed to return items in proper order or fail, so …
|
|
// … it's safe to keep some "global" state
|
|
var ordinal int
|
|
|
|
Loop:
|
|
for {
|
|
it := iter.Next()
|
|
|
|
switch {
|
|
case it.Type == pageparser.TypeIgnore:
|
|
case it.IsFrontMatter():
|
|
// Ignore.
|
|
case it.Type == pageparser.TypeLeadSummaryDivider:
|
|
posBody := -1
|
|
f := func(item pageparser.Item) bool {
|
|
if posBody == -1 && !item.IsDone() {
|
|
posBody = item.Pos()
|
|
}
|
|
|
|
if item.IsNonWhitespace(source) {
|
|
rn.summaryTruncated = true
|
|
|
|
// Done
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
iter.PeekWalk(f)
|
|
|
|
rn.hasSummaryDivider = true
|
|
|
|
// The content may be rendered by Goldmark or similar,
|
|
// and we need to track the summary.
|
|
rn.AddReplacement(internalSummaryDividerPre, it)
|
|
|
|
// Handle shortcode
|
|
case it.IsLeftShortcodeDelim():
|
|
// let extractShortcode handle left delim (will do so recursively)
|
|
iter.Backup()
|
|
|
|
currShortcode, err := s.extractShortcode(ordinal, 0, source, iter)
|
|
if err != nil {
|
|
return fail(err, it)
|
|
}
|
|
|
|
currShortcode.pos = it.Pos()
|
|
currShortcode.length = iter.Current().Pos() - it.Pos()
|
|
if currShortcode.placeholder == "" {
|
|
currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, currShortcode.ordinal)
|
|
}
|
|
|
|
if currShortcode.name != "" {
|
|
s.addName(currShortcode.name)
|
|
}
|
|
|
|
if currShortcode.params == nil {
|
|
var s []string
|
|
currShortcode.params = s
|
|
}
|
|
|
|
currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, ordinal)
|
|
ordinal++
|
|
s.shortcodes = append(s.shortcodes, currShortcode)
|
|
|
|
rn.AddShortcode(currShortcode)
|
|
|
|
case it.IsEOF():
|
|
break Loop
|
|
case it.IsError():
|
|
return fail(it.Err, it)
|
|
default:
|
|
rn.AddBytes(it)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cachedContent) mustSource() []byte {
|
|
source, err := c.pi.contentSource(c)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return source
|
|
}
|
|
|
|
func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
|
|
key := c.sourceKey
|
|
versionv := s.StaleVersion()
|
|
|
|
v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
|
|
b, err := c.readSourceAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resources.StaleValue[[]byte]{
|
|
Value: b,
|
|
StaleVersionFunc: func() uint32 {
|
|
return s.StaleVersion() - versionv
|
|
},
|
|
}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return v.Value, nil
|
|
}
|
|
|
|
func (c *contentParseInfo) readSourceAll() ([]byte, error) {
|
|
if c.openSource == nil {
|
|
return []byte{}, nil
|
|
}
|
|
r, err := c.openSource()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
type contentTableOfContents struct {
|
|
// For Goldmark we split Parse and Render.
|
|
astDoc any
|
|
|
|
tableOfContents *tableofcontents.Fragments
|
|
tableOfContentsHTML template.HTML
|
|
|
|
// Temporary storage of placeholders mapped to their content.
|
|
// These are shortcodes etc. Some of these will need to be replaced
|
|
// after any markup is rendered, so they share a common prefix.
|
|
contentPlaceholders map[string]shortcodeRenderer
|
|
|
|
contentToRender []byte
|
|
}
|
|
|
|
type contentSummary struct {
|
|
content template.HTML
|
|
summary template.HTML
|
|
summaryTruncated bool
|
|
}
|
|
|
|
type contentPlainPlainWords struct {
|
|
plain string
|
|
plainWords []string
|
|
|
|
summary template.HTML
|
|
summaryTruncated bool
|
|
|
|
wordCount int
|
|
fuzzyWordCount int
|
|
readingTime int
|
|
}
|
|
|
|
func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
|
|
ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
|
|
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
|
versionv := c.version(cp)
|
|
|
|
v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
|
|
cp.po.p.s.Log.Trace(logg.StringFunc(func() string {
|
|
return fmt.Sprintln("contentRendered", key)
|
|
}))
|
|
|
|
cp.po.p.s.h.contentRenderCounter.Add(1)
|
|
cp.contentRendered = true
|
|
po := cp.po
|
|
|
|
ct, err := c.contentToC(ctx, cp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rs := &resources.StaleValue[contentSummary]{
|
|
StaleVersionFunc: func() uint32 {
|
|
return c.version(cp) - versionv
|
|
},
|
|
}
|
|
|
|
if len(c.pi.itemsStep2) == 0 {
|
|
// Nothing to do.
|
|
return rs, nil
|
|
}
|
|
|
|
var b []byte
|
|
|
|
if ct.astDoc != nil {
|
|
// The content is parsed, but not rendered.
|
|
r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ok {
|
|
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
|
|
}
|
|
|
|
b = r.Bytes()
|
|
|
|
} else {
|
|
// Copy the content to be rendered.
|
|
b = make([]byte, len(ct.contentToRender))
|
|
copy(b, ct.contentToRender)
|
|
}
|
|
|
|
// There are one or more replacement tokens to be replaced.
|
|
var hasShortcodeVariants bool
|
|
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
|
|
if token == tocShortcodePlaceholder {
|
|
return []byte(ct.tableOfContentsHTML), nil
|
|
}
|
|
renderer, found := ct.contentPlaceholders[token]
|
|
if found {
|
|
repl, more, err := renderer.renderShortcode(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hasShortcodeVariants = hasShortcodeVariants || more
|
|
return repl, nil
|
|
}
|
|
// This should never happen.
|
|
panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders)))
|
|
}
|
|
|
|
b, err = expandShortcodeTokens(ctx, b, tokenHandler)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasShortcodeVariants {
|
|
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
|
}
|
|
|
|
var result contentSummary // hasVariants bool
|
|
|
|
if c.pi.hasSummaryDivider {
|
|
if cp.po.p.m.pageConfig.ContentMediaType.IsHTML() {
|
|
// Use the summary sections as provided by the user.
|
|
i := bytes.Index(b, internalSummaryDividerPre)
|
|
result.summary = helpers.BytesToHTML(b[:i])
|
|
b = b[i+len(internalSummaryDividerPre):]
|
|
|
|
} else {
|
|
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Content.Markup, b)
|
|
if err != nil {
|
|
cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err)
|
|
} else {
|
|
b = content
|
|
result.summary = helpers.BytesToHTML(summary)
|
|
}
|
|
}
|
|
result.summaryTruncated = c.pi.summaryTruncated
|
|
}
|
|
result.content = helpers.BytesToHTML(b)
|
|
rs.Value = result
|
|
|
|
return rs, nil
|
|
})
|
|
if err != nil {
|
|
return contentSummary{}, cp.po.p.wrapError(err)
|
|
}
|
|
|
|
return v.Value, nil
|
|
}
|
|
|
|
func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutput) contentTableOfContents {
|
|
ct, err := c.contentToC(ctx, cp)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ct
|
|
}
|
|
|
|
var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback")
|
|
|
|
func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) {
|
|
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
|
versionv := c.version(cp)
|
|
|
|
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
|
|
source, err := c.pi.contentSource(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ct contentTableOfContents
|
|
if err := cp.initRenderHooks(); err != nil {
|
|
return nil, err
|
|
}
|
|
f := cp.po.f
|
|
po := cp.po
|
|
p := po.p
|
|
ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Callback called from below (e.g. in .RenderString)
|
|
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
|
|
cp.otherOutputs[cp2.po.p.pid] = cp2
|
|
|
|
// Merge content placeholders
|
|
for k, v := range ct2.contentPlaceholders {
|
|
ct.contentPlaceholders[k] = v
|
|
}
|
|
|
|
if p.s.conf.Internal.Watch {
|
|
for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
|
|
for _, templ := range s.templs {
|
|
cp.trackDependency(templ.(identity.IdentityProvider))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
|
|
cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
|
|
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
|
|
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
|
}
|
|
}
|
|
|
|
ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
|
|
|
|
var hasVariants bool
|
|
ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if hasVariants {
|
|
p.pageOutputTemplateVariationsState.Add(1)
|
|
}
|
|
|
|
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
|
|
|
|
if !isHTML {
|
|
createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
|
|
cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
|
|
ct.tableOfContents = tocProvider.TableOfContents()
|
|
ct.tableOfContentsHTML = template.HTML(
|
|
ct.tableOfContents.ToHTML(
|
|
cfg.TableOfContents.StartLevel,
|
|
cfg.TableOfContents.EndLevel,
|
|
cfg.TableOfContents.Ordered,
|
|
),
|
|
)
|
|
}
|
|
|
|
// If the converter supports doing the parsing separately, we do that.
|
|
parseResult, ok, err := po.contentRenderer.ParseContent(ctx, ct.contentToRender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ok {
|
|
// This is Goldmark.
|
|
// Store away the parse result for later use.
|
|
createAndSetToC(parseResult)
|
|
|
|
ct.astDoc = parseResult.Doc()
|
|
|
|
} else {
|
|
|
|
// This is Asciidoctor etc.
|
|
r, err := po.contentRenderer.ParseAndRenderContent(ctx, ct.contentToRender, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ct.contentToRender = r.Bytes()
|
|
|
|
if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
|
|
createAndSetToC(tocProvider)
|
|
} else {
|
|
tmpContent, tmpTableOfContents := helpers.ExtractTOC(ct.contentToRender)
|
|
ct.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents)
|
|
ct.tableOfContents = tableofcontents.Empty
|
|
ct.contentToRender = tmpContent
|
|
}
|
|
}
|
|
}
|
|
|
|
return &resources.StaleValue[contentTableOfContents]{
|
|
Value: ct,
|
|
StaleVersionFunc: func() uint32 {
|
|
return c.version(cp) - versionv
|
|
},
|
|
}, nil
|
|
})
|
|
if err != nil {
|
|
return contentTableOfContents{}, err
|
|
}
|
|
|
|
return v.Value, nil
|
|
}
|
|
|
|
func (c *cachedContent) version(cp *pageContentOutput) uint32 {
|
|
// Both of these gets incremented on change.
|
|
return c.StaleVersion() + cp.contentRenderedVersion
|
|
}
|
|
|
|
func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) {
|
|
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
|
|
|
versionv := c.version(cp)
|
|
|
|
v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) {
|
|
var result contentPlainPlainWords
|
|
rs := &resources.StaleValue[contentPlainPlainWords]{
|
|
StaleVersionFunc: func() uint32 {
|
|
return c.version(cp) - versionv
|
|
},
|
|
}
|
|
|
|
rendered, err := c.contentRendered(ctx, cp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.plain = tpl.StripHTML(string(rendered.content))
|
|
result.plainWords = strings.Fields(result.plain)
|
|
|
|
isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
|
|
|
|
if isCJKLanguage {
|
|
result.wordCount = 0
|
|
for _, word := range result.plainWords {
|
|
runeCount := utf8.RuneCountInString(word)
|
|
if len(word) == runeCount {
|
|
result.wordCount++
|
|
} else {
|
|
result.wordCount += runeCount
|
|
}
|
|
}
|
|
} else {
|
|
result.wordCount = helpers.TotalWords(result.plain)
|
|
}
|
|
|
|
// TODO(bep) is set in a test. Fix that.
|
|
if result.fuzzyWordCount == 0 {
|
|
result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100
|
|
}
|
|
|
|
if isCJKLanguage {
|
|
result.readingTime = (result.wordCount + 500) / 501
|
|
} else {
|
|
result.readingTime = (result.wordCount + 212) / 213
|
|
}
|
|
|
|
if c.pi.hasSummaryDivider || rendered.summary != "" {
|
|
result.summary = rendered.summary
|
|
result.summaryTruncated = rendered.summaryTruncated
|
|
} else if cp.po.p.m.pageConfig.Summary != "" {
|
|
b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
|
|
result.summary = helpers.BytesToHTML(html)
|
|
} else {
|
|
var summary string
|
|
var truncated bool
|
|
if isCJKLanguage {
|
|
summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsByRune(result.plainWords)
|
|
} else {
|
|
summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsToWholeSentence(result.plain)
|
|
}
|
|
result.summary = template.HTML(summary)
|
|
result.summaryTruncated = truncated
|
|
}
|
|
|
|
rs.Value = result
|
|
|
|
return rs, nil
|
|
})
|
|
if err != nil {
|
|
if herrors.IsTimeoutError(err) {
|
|
err = fmt.Errorf("timed out rendering the page content. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file: %w", err)
|
|
}
|
|
return contentPlainPlainWords{}, err
|
|
}
|
|
return v.Value, nil
|
|
}
|