URI: 
       page__content.go - hugo - [fork] hugo port for 9front
  HTML git clone https://git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
       page__content.go (30685B)
       ---
            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         "context"
           18         "errors"
           19         "fmt"
           20         "html/template"
           21         "io"
           22         "path/filepath"
           23         "strconv"
           24         "strings"
           25         "unicode/utf8"
           26 
           27         maps0 "maps"
           28 
           29         "github.com/bep/logg"
           30         "github.com/gohugoio/hugo/common/hcontext"
           31         "github.com/gohugoio/hugo/common/herrors"
           32         "github.com/gohugoio/hugo/common/hugio"
           33         "github.com/gohugoio/hugo/common/hugo"
           34         "github.com/gohugoio/hugo/common/maps"
           35         "github.com/gohugoio/hugo/common/types/hstring"
           36         "github.com/gohugoio/hugo/helpers"
           37         "github.com/gohugoio/hugo/markup"
           38         "github.com/gohugoio/hugo/markup/converter"
           39         "github.com/gohugoio/hugo/markup/goldmark/hugocontext"
           40         "github.com/gohugoio/hugo/markup/tableofcontents"
           41         "github.com/gohugoio/hugo/parser/metadecoders"
           42         "github.com/gohugoio/hugo/parser/pageparser"
           43         "github.com/gohugoio/hugo/resources"
           44         "github.com/gohugoio/hugo/resources/page"
           45         "github.com/gohugoio/hugo/resources/resource"
           46         "github.com/gohugoio/hugo/tpl"
           47         "github.com/mitchellh/mapstructure"
           48         "github.com/spf13/cast"
           49 )
           50 
           51 const (
           52         internalSummaryDividerBase = "HUGOMORE42"
           53 )
           54 
           55 var (
           56         internalSummaryDividerPreString = "\n\n" + internalSummaryDividerBase + "\n\n"
           57         internalSummaryDividerPre       = []byte(internalSummaryDividerPreString)
           58 )
           59 
           60 type pageContentReplacement struct {
           61         val []byte
           62 
           63         source pageparser.Item
           64 }
           65 
           66 func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) {
           67         var (
           68                 sourceKey            string
           69                 openSource           hugio.OpenReadSeekCloser
           70                 isFromContentAdapter = m.pageConfig.IsFromContentAdapter
           71         )
           72 
           73         if m.f != nil && !isFromContentAdapter {
           74                 sourceKey = filepath.ToSlash(m.f.Filename())
           75                 if !isFromContentAdapter {
           76                         meta := m.f.FileInfo().Meta()
           77                         openSource = func() (hugio.ReadSeekCloser, error) {
           78                                 r, err := meta.Open()
           79                                 if err != nil {
           80                                         return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
           81                                 }
           82                                 return r, nil
           83                         }
           84                 }
           85         } else if isFromContentAdapter {
           86                 openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser()
           87         }
           88 
           89         if sourceKey == "" {
           90                 sourceKey = strconv.FormatUint(pid, 10)
           91         }
           92 
           93         pi := &contentParseInfo{
           94                 h:          h,
           95                 pid:        pid,
           96                 sourceKey:  sourceKey,
           97                 openSource: openSource,
           98         }
           99 
          100         source, err := pi.contentSource(m)
          101         if err != nil {
          102                 return nil, err
          103         }
          104 
          105         items, err := pageparser.ParseBytes(
          106                 source,
          107                 pageparser.Config{
          108                         NoFrontMatter: isFromContentAdapter,
          109                 },
          110         )
          111         if err != nil {
          112                 return nil, err
          113         }
          114 
          115         pi.itemsStep1 = items
          116 
          117         if isFromContentAdapter {
          118                 // No front matter.
          119                 return pi, nil
          120         }
          121 
          122         if err := pi.mapFrontMatter(source); err != nil {
          123                 return nil, err
          124         }
          125 
          126         return pi, nil
          127 }
          128 
          129 func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) {
          130         var filename string
          131         if m.f != nil {
          132                 filename = m.f.Filename()
          133         }
          134 
          135         c := &cachedContent{
          136                 pm:             m.s.pageMap,
          137                 StaleInfo:      m,
          138                 shortcodeState: newShortcodeHandler(filename, m.s),
          139                 pi:             pi,
          140                 enableEmoji:    m.s.conf.EnableEmoji,
          141                 scopes:         maps.NewCache[string, *cachedContentScope](),
          142         }
          143 
          144         source, err := c.pi.contentSource(m)
          145         if err != nil {
          146                 return nil, err
          147         }
          148 
          149         if err := c.parseContentFile(source); err != nil {
          150                 return nil, err
          151         }
          152 
          153         return c, nil
          154 }
          155 
          156 type cachedContent struct {
          157         pm *pageMap
          158 
          159         resource.StaleInfo
          160 
          161         shortcodeState *shortcodeHandler
          162 
          163         // Parsed content.
          164         pi *contentParseInfo
          165 
          166         enableEmoji bool
          167 
          168         scopes *maps.Cache[string, *cachedContentScope]
          169 }
          170 
          171 func (c *cachedContent) getOrCreateScope(scope string, pco *pageContentOutput) *cachedContentScope {
          172         key := scope + pco.po.f.Name
          173         cs, _ := c.scopes.GetOrCreate(key, func() (*cachedContentScope, error) {
          174                 return &cachedContentScope{
          175                         cachedContent: c,
          176                         pco:           pco,
          177                         scope:         scope,
          178                 }, nil
          179         })
          180         return cs
          181 }
          182 
          183 type contentParseInfo struct {
          184         h *HugoSites
          185 
          186         pid       uint64
          187         sourceKey string
          188 
          189         // The source bytes.
          190         openSource hugio.OpenReadSeekCloser
          191 
          192         frontMatter map[string]any
          193 
          194         // Whether the parsed content contains a summary separator.
          195         hasSummaryDivider bool
          196 
          197         // Returns the position in bytes after any front matter.
          198         posMainContent int
          199 
          200         // Indicates whether we must do placeholder replacements.
          201         hasNonMarkdownShortcode bool
          202 
          203         // Items from the page parser.
          204         // These maps directly to the source
          205         itemsStep1 pageparser.Items
          206 
          207         //  *shortcode, pageContentReplacement or pageparser.Item
          208         itemsStep2 []any
          209 }
          210 
          211 func (p *contentParseInfo) AddBytes(item pageparser.Item) {
          212         p.itemsStep2 = append(p.itemsStep2, item)
          213 }
          214 
          215 func (p *contentParseInfo) AddReplacement(val []byte, source pageparser.Item) {
          216         p.itemsStep2 = append(p.itemsStep2, pageContentReplacement{val: val, source: source})
          217 }
          218 
          219 func (p *contentParseInfo) AddShortcode(s *shortcode) {
          220         p.itemsStep2 = append(p.itemsStep2, s)
          221         if s.insertPlaceholder() {
          222                 p.hasNonMarkdownShortcode = true
          223         }
          224 }
          225 
          226 // contentToRenderForItems returns the content to be processed by Goldmark or similar.
          227 func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) {
          228         var hasVariants bool
          229         c := make([]byte, 0, len(source)+(len(source)/10))
          230 
          231         for _, it := range pi.itemsStep2 {
          232                 switch v := it.(type) {
          233                 case pageparser.Item:
          234                         c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
          235                 case pageContentReplacement:
          236                         c = append(c, v.val...)
          237                 case *shortcode:
          238                         if !v.insertPlaceholder() {
          239                                 // Insert the rendered shortcode.
          240                                 renderedShortcode, found := renderedShortcodes[v.placeholder]
          241                                 if !found {
          242                                         // This should never happen.
          243                                         panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
          244                                 }
          245 
          246                                 b, more, err := renderedShortcode.renderShortcode(ctx)
          247                                 if err != nil {
          248                                         return nil, false, fmt.Errorf("failed to render shortcode: %w", err)
          249                                 }
          250                                 hasVariants = hasVariants || more
          251                                 c = append(c, []byte(b)...)
          252 
          253                         } else {
          254                                 // Insert the placeholder so we can insert the content after
          255                                 // markdown processing.
          256                                 c = append(c, []byte(v.placeholder)...)
          257                         }
          258                 default:
          259                         panic(fmt.Sprintf("unknown item type %T", it))
          260                 }
          261         }
          262 
          263         return c, hasVariants, nil
          264 }
          265 
          266 func (c *cachedContent) IsZero() bool {
          267         return len(c.pi.itemsStep2) == 0
          268 }
          269 
          270 func (c *cachedContent) parseContentFile(source []byte) error {
          271         if source == nil || c.pi.openSource == nil {
          272                 return nil
          273         }
          274 
          275         return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState)
          276 }
          277 
          278 func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error {
          279         if c.frontMatter != nil {
          280                 return nil
          281         }
          282 
          283         f := pageparser.FormatFromFrontMatterType(it.Type)
          284         var err error
          285         c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f)
          286         if err != nil {
          287                 if fe, ok := err.(herrors.FileError); ok {
          288                         pos := fe.Position()
          289 
          290                         // Offset the starting position of front matter.
          291                         offset := iter.LineNumber(source) - 1
          292                         if f == metadecoders.YAML {
          293                                 offset -= 1
          294                         }
          295                         pos.LineNumber += offset
          296 
          297                         fe.UpdatePosition(pos)
          298                         fe.SetFilename("") // It will be set later.
          299 
          300                         return fe
          301                 } else {
          302                         return err
          303                 }
          304         }
          305 
          306         return nil
          307 }
          308 
          309 func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error {
          310         if fe, ok := err.(herrors.FileError); ok {
          311                 return fe
          312         }
          313 
          314         pos := posFromInput("", source, i.Pos())
          315 
          316         return herrors.NewFileErrorFromPos(err, pos)
          317 }
          318 
          319 func (rn *contentParseInfo) mapFrontMatter(source []byte) error {
          320         if len(rn.itemsStep1) == 0 {
          321                 return nil
          322         }
          323         iter := pageparser.NewIterator(rn.itemsStep1)
          324 
          325 Loop:
          326         for {
          327                 it := iter.Next()
          328                 switch {
          329                 case it.IsFrontMatter():
          330                         if err := rn.parseFrontMatter(it, iter, source); err != nil {
          331                                 return err
          332                         }
          333                         next := iter.Peek()
          334                         if !next.IsDone() {
          335                                 rn.posMainContent = next.Pos()
          336                         }
          337                         // Done.
          338                         break Loop
          339                 case it.IsEOF():
          340                         break Loop
          341                 case it.IsError():
          342                         return rn.failMap(source, it.Err, it)
          343                 default:
          344 
          345                 }
          346         }
          347 
          348         return nil
          349 }
          350 
          351 func (rn *contentParseInfo) mapItemsAfterFrontMatter(
          352         source []byte,
          353         s *shortcodeHandler,
          354 ) error {
          355         if len(rn.itemsStep1) == 0 {
          356                 return nil
          357         }
          358 
          359         fail := func(err error, i pageparser.Item) error {
          360                 if fe, ok := err.(herrors.FileError); ok {
          361                         return fe
          362                 }
          363 
          364                 pos := posFromInput("", source, i.Pos())
          365 
          366                 return herrors.NewFileErrorFromPos(err, pos)
          367         }
          368 
          369         iter := pageparser.NewIterator(rn.itemsStep1)
          370 
          371         // the parser is guaranteed to return items in proper order or fail, so …
          372         // … it's safe to keep some "global" state
          373         var ordinal int
          374 
          375 Loop:
          376         for {
          377                 it := iter.Next()
          378 
          379                 switch {
          380                 case it.Type == pageparser.TypeIgnore:
          381                 case it.IsFrontMatter():
          382                         // Ignore.
          383                 case it.Type == pageparser.TypeLeadSummaryDivider:
          384                         posBody := -1
          385                         f := func(item pageparser.Item) bool {
          386                                 if posBody == -1 && !item.IsDone() {
          387                                         posBody = item.Pos()
          388                                 }
          389 
          390                                 if item.IsNonWhitespace(source) {
          391                                         // Done
          392                                         return false
          393                                 }
          394                                 return true
          395                         }
          396                         iter.PeekWalk(f)
          397 
          398                         rn.hasSummaryDivider = true
          399 
          400                         // The content may be rendered by Goldmark or similar,
          401                         // and we need to track the summary.
          402                         rn.AddReplacement(internalSummaryDividerPre, it)
          403 
          404                 // Handle shortcode
          405                 case it.IsLeftShortcodeDelim():
          406                         // let extractShortcode handle left delim (will do so recursively)
          407                         iter.Backup()
          408 
          409                         currShortcode, err := s.extractShortcode(ordinal, 0, source, iter)
          410                         if err != nil {
          411                                 return fail(err, it)
          412                         }
          413 
          414                         currShortcode.pos = it.Pos()
          415                         currShortcode.length = iter.Current().Pos() - it.Pos()
          416                         if currShortcode.placeholder == "" {
          417                                 currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, currShortcode.ordinal)
          418                         }
          419 
          420                         if currShortcode.name != "" {
          421                                 s.addName(currShortcode.name)
          422                         }
          423 
          424                         if currShortcode.params == nil {
          425                                 var s []string
          426                                 currShortcode.params = s
          427                         }
          428 
          429                         currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, ordinal)
          430                         ordinal++
          431                         s.shortcodes = append(s.shortcodes, currShortcode)
          432 
          433                         rn.AddShortcode(currShortcode)
          434 
          435                 case it.IsEOF():
          436                         break Loop
          437                 case it.IsError():
          438                         return fail(it.Err, it)
          439                 default:
          440                         rn.AddBytes(it)
          441                 }
          442         }
          443 
          444         return nil
          445 }
          446 
          447 func (c *cachedContent) mustSource() []byte {
          448         source, err := c.pi.contentSource(c)
          449         if err != nil {
          450                 panic(err)
          451         }
          452         return source
          453 }
          454 
          455 func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
          456         key := c.sourceKey
          457         versionv := s.StaleVersion()
          458 
          459         v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
          460                 b, err := c.readSourceAll()
          461                 if err != nil {
          462                         return nil, err
          463                 }
          464 
          465                 return &resources.StaleValue[[]byte]{
          466                         Value: b,
          467                         StaleVersionFunc: func() uint32 {
          468                                 return s.StaleVersion() - versionv
          469                         },
          470                 }, nil
          471         })
          472         if err != nil {
          473                 return nil, err
          474         }
          475 
          476         return v.Value, nil
          477 }
          478 
          479 func (c *contentParseInfo) readSourceAll() ([]byte, error) {
          480         if c.openSource == nil {
          481                 return []byte{}, nil
          482         }
          483         r, err := c.openSource()
          484         if err != nil {
          485                 return nil, err
          486         }
          487         defer r.Close()
          488 
          489         return io.ReadAll(r)
          490 }
          491 
          492 type contentTableOfContents struct {
          493         // For Goldmark we split Parse and Render.
          494         astDoc any
          495 
          496         tableOfContents     *tableofcontents.Fragments
          497         tableOfContentsHTML template.HTML
          498 
          499         // Temporary storage of placeholders mapped to their content.
          500         // These are shortcodes etc. Some of these will need to be replaced
          501         // after any markup is rendered, so they share a common prefix.
          502         contentPlaceholders map[string]shortcodeRenderer
          503 
          504         contentToRender []byte
          505 }
          506 
          507 type contentSummary struct {
          508         content               template.HTML
          509         contentWithoutSummary template.HTML
          510         summary               page.Summary
          511 }
          512 
          513 type contentPlainPlainWords struct {
          514         plain      string
          515         plainWords []string
          516 
          517         wordCount      int
          518         fuzzyWordCount int
          519         readingTime    int
          520 }
          521 
          522 func (c *cachedContentScope) keyScope(ctx context.Context) string {
          523         return hugo.GetMarkupScope(ctx) + c.pco.po.f.Name
          524 }
          525 
          526 func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummary, error) {
          527         cp := c.pco
          528         ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
          529         key := c.pi.sourceKey + "/" + c.keyScope(ctx)
          530         versionv := c.version(cp)
          531 
          532         v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
          533                 cp.po.p.s.Log.Trace(logg.StringFunc(func() string {
          534                         return fmt.Sprintln("contentRendered", key)
          535                 }))
          536 
          537                 cp.po.p.s.h.contentRenderCounter.Add(1)
          538                 cp.contentRendered.Store(true)
          539                 po := cp.po
          540 
          541                 ct, err := c.contentToC(ctx)
          542                 if err != nil {
          543                         return nil, err
          544                 }
          545 
          546                 rs, err := func() (*resources.StaleValue[contentSummary], error) {
          547                         rs := &resources.StaleValue[contentSummary]{
          548                                 StaleVersionFunc: func() uint32 {
          549                                         return c.version(cp) - versionv
          550                                 },
          551                         }
          552 
          553                         if len(c.pi.itemsStep2) == 0 {
          554                                 // Nothing to do.
          555                                 return rs, nil
          556                         }
          557 
          558                         var b []byte
          559 
          560                         if ct.astDoc != nil {
          561                                 // The content is parsed, but not rendered.
          562                                 r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
          563                                 if err != nil {
          564                                         return nil, err
          565                                 }
          566 
          567                                 if !ok {
          568                                         return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
          569                                 }
          570 
          571                                 b = r.Bytes()
          572 
          573                         } else {
          574                                 // Copy the content to be rendered.
          575                                 b = make([]byte, len(ct.contentToRender))
          576                                 copy(b, ct.contentToRender)
          577                         }
          578 
          579                         // There are one or more replacement tokens to be replaced.
          580                         var hasShortcodeVariants bool
          581                         tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
          582                                 if token == tocShortcodePlaceholder {
          583                                         return []byte(ct.tableOfContentsHTML), nil
          584                                 }
          585                                 renderer, found := ct.contentPlaceholders[token]
          586                                 if found {
          587                                         repl, more, err := renderer.renderShortcode(ctx)
          588                                         if err != nil {
          589                                                 return nil, err
          590                                         }
          591                                         hasShortcodeVariants = hasShortcodeVariants || more
          592                                         return repl, nil
          593                                 }
          594                                 // This should never happen.
          595                                 panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders)))
          596                         }
          597 
          598                         b, err = expandShortcodeTokens(ctx, b, tokenHandler)
          599                         if err != nil {
          600                                 return nil, err
          601                         }
          602                         if hasShortcodeVariants {
          603                                 cp.po.p.incrPageOutputTemplateVariation()
          604                         }
          605 
          606                         var result contentSummary
          607                         if c.pi.hasSummaryDivider {
          608                                 s := string(b)
          609                                 summarized := page.ExtractSummaryFromHTMLWithDivider(cp.po.p.m.pageConfig.ContentMediaType, s, internalSummaryDividerBase)
          610                                 result.summary = page.Summary{
          611                                         Text:      template.HTML(summarized.Summary()),
          612                                         Type:      page.SummaryTypeManual,
          613                                         Truncated: summarized.Truncated(),
          614                                 }
          615                                 result.contentWithoutSummary = template.HTML(summarized.ContentWithoutSummary())
          616                                 result.content = template.HTML(summarized.Content())
          617                         } else {
          618                                 result.content = template.HTML(string(b))
          619                         }
          620 
          621                         if !c.pi.hasSummaryDivider && cp.po.p.m.pageConfig.Summary == "" {
          622                                 numWords := cp.po.p.s.conf.SummaryLength
          623                                 isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
          624                                 summary := page.ExtractSummaryFromHTML(cp.po.p.m.pageConfig.ContentMediaType, string(result.content), numWords, isCJKLanguage)
          625                                 result.summary = page.Summary{
          626                                         Text:      template.HTML(summary.Summary()),
          627                                         Type:      page.SummaryTypeAuto,
          628                                         Truncated: summary.Truncated(),
          629                                 }
          630                                 result.contentWithoutSummary = template.HTML(summary.ContentWithoutSummary())
          631                         }
          632                         rs.Value = result
          633 
          634                         return rs, nil
          635                 }()
          636                 if err != nil {
          637                         return rs, cp.po.p.wrapError(err)
          638                 }
          639 
          640                 if rs.Value.summary.IsZero() {
          641                         b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
          642                         if err != nil {
          643                                 return nil, err
          644                         }
          645                         html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
          646                         rs.Value.summary = page.Summary{
          647                                 Text: helpers.BytesToHTML(html),
          648                                 Type: page.SummaryTypeFrontMatter,
          649                         }
          650                         rs.Value.contentWithoutSummary = rs.Value.content
          651                 }
          652 
          653                 return rs, err
          654         })
          655         if err != nil {
          656                 return contentSummary{}, cp.po.p.wrapError(err)
          657         }
          658 
          659         return v.Value, nil
          660 }
          661 
          662 func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfContents {
          663         ct, err := c.contentToC(ctx)
          664         if err != nil {
          665                 panic(err)
          666         }
          667         return ct
          668 }
          669 
          670 type contextKey uint8
          671 
          672 const (
          673         contextKeyContentCallback contextKey = iota
          674 )
          675 
          676 var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)](contextKeyContentCallback)
          677 
          678 func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) {
          679         cp := c.pco
          680         key := c.pi.sourceKey + "/" + c.keyScope(ctx)
          681         versionv := c.version(cp)
          682 
          683         v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
          684                 source, err := c.pi.contentSource(c)
          685                 if err != nil {
          686                         return nil, err
          687                 }
          688 
          689                 var ct contentTableOfContents
          690                 if err := cp.initRenderHooks(); err != nil {
          691                         return nil, err
          692                 }
          693                 po := cp.po
          694                 p := po.p
          695                 ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
          696                 if err != nil {
          697                         return nil, err
          698                 }
          699 
          700                 // Callback called from below (e.g. in .RenderString)
          701                 ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
          702                         cp.otherOutputs.Set(cp2.po.p.pid, cp2)
          703 
          704                         // Merge content placeholders
          705                         maps0.Copy(ct.contentPlaceholders, ct2.contentPlaceholders)
          706 
          707                         if p.s.conf.Internal.Watch {
          708                                 for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
          709                                         cp.trackDependency(s.templ)
          710                                 }
          711                         }
          712 
          713                         // Transfer shortcode names so HasShortcode works for shortcodes from included pages.
          714                         cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
          715                         if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
          716                                 cp.po.p.incrPageOutputTemplateVariation()
          717                         }
          718                 }
          719 
          720                 ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
          721 
          722                 var hasVariants bool
          723                 ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
          724                 if err != nil {
          725                         return nil, err
          726                 }
          727 
          728                 if hasVariants {
          729                         p.incrPageOutputTemplateVariation()
          730                 }
          731 
          732                 isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
          733 
          734                 if !isHTML {
          735                         createAndSetToC := func(tocProvider converter.TableOfContentsProvider) error {
          736                                 cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
          737                                 ct.tableOfContents = tocProvider.TableOfContents()
          738                                 ct.tableOfContentsHTML, err = ct.tableOfContents.ToHTML(
          739                                         cfg.TableOfContents.StartLevel,
          740                                         cfg.TableOfContents.EndLevel,
          741                                         cfg.TableOfContents.Ordered,
          742                                 )
          743                                 return err
          744                         }
          745 
          746                         // If the converter supports doing the parsing separately, we do that.
          747                         parseResult, ok, err := po.contentRenderer.ParseContent(ctx, ct.contentToRender)
          748                         if err != nil {
          749                                 return nil, err
          750                         }
          751                         if ok {
          752                                 // This is Goldmark.
          753                                 // Store away the parse result for later use.
          754                                 createAndSetToC(parseResult)
          755 
          756                                 ct.astDoc = parseResult.Doc()
          757 
          758                         } else {
          759 
          760                                 // This is Asciidoctor etc.
          761                                 r, err := po.contentRenderer.ParseAndRenderContent(ctx, ct.contentToRender, true)
          762                                 if err != nil {
          763                                         return nil, err
          764                                 }
          765 
          766                                 ct.contentToRender = r.Bytes()
          767 
          768                                 if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
          769                                         createAndSetToC(tocProvider)
          770                                 } else {
          771                                         tmpContent, tmpTableOfContents := helpers.ExtractTOC(ct.contentToRender)
          772                                         ct.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents)
          773                                         ct.tableOfContents = tableofcontents.Empty
          774                                         ct.contentToRender = tmpContent
          775                                 }
          776                         }
          777                 }
          778 
          779                 return &resources.StaleValue[contentTableOfContents]{
          780                         Value: ct,
          781                         StaleVersionFunc: func() uint32 {
          782                                 return c.version(cp) - versionv
          783                         },
          784                 }, nil
          785         })
          786         if err != nil {
          787                 return contentTableOfContents{}, err
          788         }
          789 
          790         return v.Value, nil
          791 }
          792 
          793 func (c *cachedContent) version(cp *pageContentOutput) uint32 {
          794         // Both of these gets incremented on change.
          795         return c.StaleVersion() + cp.contentRenderedVersion
          796 }
          797 
          798 func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlainWords, error) {
          799         cp := c.pco
          800         key := c.pi.sourceKey + "/" + c.keyScope(ctx)
          801 
          802         versionv := c.version(cp)
          803 
          804         v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) {
          805                 var result contentPlainPlainWords
          806                 rs := &resources.StaleValue[contentPlainPlainWords]{
          807                         StaleVersionFunc: func() uint32 {
          808                                 return c.version(cp) - versionv
          809                         },
          810                 }
          811 
          812                 rendered, err := c.contentRendered(ctx)
          813                 if err != nil {
          814                         return nil, err
          815                 }
          816 
          817                 result.plain = tpl.StripHTML(string(rendered.content))
          818                 result.plainWords = strings.Fields(result.plain)
          819 
          820                 isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
          821 
          822                 if isCJKLanguage {
          823                         result.wordCount = 0
          824                         for _, word := range result.plainWords {
          825                                 runeCount := utf8.RuneCountInString(word)
          826                                 if len(word) == runeCount {
          827                                         result.wordCount++
          828                                 } else {
          829                                         result.wordCount += runeCount
          830                                 }
          831                         }
          832                 } else {
          833                         result.wordCount = helpers.TotalWords(result.plain)
          834                 }
          835 
          836                 // TODO(bep) is set in a test. Fix that.
          837                 if result.fuzzyWordCount == 0 {
          838                         result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100
          839                 }
          840 
          841                 if isCJKLanguage {
          842                         result.readingTime = (result.wordCount + 500) / 501
          843                 } else {
          844                         result.readingTime = (result.wordCount + 212) / 213
          845                 }
          846 
          847                 rs.Value = result
          848 
          849                 return rs, nil
          850         })
          851         if err != nil {
          852                 if herrors.IsTimeoutError(err) {
          853                         err = fmt.Errorf("timed out rendering the page content. Extend the `timeout` limit in your Hugo config file: %w", err)
          854                 }
          855                 return contentPlainPlainWords{}, err
          856         }
          857         return v.Value, nil
          858 }
          859 
          860 type cachedContentScope struct {
          861         *cachedContent
          862         pco   *pageContentOutput
          863         scope string
          864 }
          865 
          866 func (c *cachedContentScope) prepareContext(ctx context.Context) context.Context {
          867         // A regular page's shortcode etc. may be rendered by e.g. the home page,
          868         // so we need to track any changes to this content's page.
          869         ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, c.pco.po.p)
          870 
          871         // The markup scope is recursive, so if already set to a non zero value, preserve that value.
          872         if s := hugo.GetMarkupScope(ctx); s != "" || s == c.scope {
          873                 return ctx
          874         }
          875         return hugo.SetMarkupScope(ctx, c.scope)
          876 }
          877 
          878 func (c *cachedContentScope) Render(ctx context.Context) (page.Content, error) {
          879         return c, nil
          880 }
          881 
          882 func (c *cachedContentScope) Content(ctx context.Context) (template.HTML, error) {
          883         ctx = c.prepareContext(ctx)
          884         cr, err := c.contentRendered(ctx)
          885         if err != nil {
          886                 return "", err
          887         }
          888         return cr.content, nil
          889 }
          890 
          891 func (c *cachedContentScope) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
          892         ctx = c.prepareContext(ctx)
          893         cr, err := c.contentRendered(ctx)
          894         if err != nil {
          895                 return "", err
          896         }
          897         return cr.contentWithoutSummary, nil
          898 }
          899 
          900 func (c *cachedContentScope) Summary(ctx context.Context) (page.Summary, error) {
          901         ctx = c.prepareContext(ctx)
          902         rendered, err := c.contentRendered(ctx)
          903         return rendered.summary, err
          904 }
          905 
          906 func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
          907         ctx = c.prepareContext(ctx)
          908 
          909         if len(args) < 1 || len(args) > 2 {
          910                 return "", errors.New("want 1 or 2 arguments")
          911         }
          912 
          913         pco := c.pco
          914 
          915         var contentToRender string
          916         opts := defaultRenderStringOpts
          917         sidx := 1
          918 
          919         if len(args) == 1 {
          920                 sidx = 0
          921         } else {
          922                 m, ok := args[0].(map[string]any)
          923                 if !ok {
          924                         return "", errors.New("first argument must be a map")
          925                 }
          926 
          927                 if err := mapstructure.WeakDecode(m, &opts); err != nil {
          928                         return "", fmt.Errorf("failed to decode options: %w", err)
          929                 }
          930                 if opts.Markup != "" {
          931                         opts.Markup = markup.ResolveMarkup(opts.Markup)
          932                 }
          933         }
          934 
          935         contentToRenderv := args[sidx]
          936 
          937         if _, ok := contentToRenderv.(hstring.HTML); ok {
          938                 // This content is already rendered, this is potentially
          939                 // a infinite recursion.
          940                 return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
          941         }
          942 
          943         var err error
          944         contentToRender, err = cast.ToStringE(contentToRenderv)
          945         if err != nil {
          946                 return "", err
          947         }
          948 
          949         if err = pco.initRenderHooks(); err != nil {
          950                 return "", err
          951         }
          952 
          953         conv := pco.po.p.getContentConverter()
          954 
          955         if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType {
          956                 var err error
          957                 conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
          958                 if err != nil {
          959                         return "", pco.po.p.wrapError(err)
          960                 }
          961         }
          962 
          963         var rendered []byte
          964 
          965         parseInfo := &contentParseInfo{
          966                 h:   pco.po.p.s.h,
          967                 pid: pco.po.p.pid,
          968         }
          969 
          970         if pageparser.HasShortcode(contentToRender) {
          971                 contentToRenderb := []byte(contentToRender)
          972                 // String contains a shortcode.
          973                 parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
          974                         NoFrontMatter:    true,
          975                         NoSummaryDivider: true,
          976                 })
          977                 if err != nil {
          978                         return "", err
          979                 }
          980 
          981                 s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
          982                 if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
          983                         return "", err
          984                 }
          985 
          986                 placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
          987                 if err != nil {
          988                         return "", err
          989                 }
          990 
          991                 contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
          992                 if err != nil {
          993                         return "", err
          994                 }
          995                 if hasVariants {
          996                         pco.po.p.incrPageOutputTemplateVariation()
          997                 }
          998                 b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
          999                 if err != nil {
         1000                         return "", pco.po.p.wrapError(err)
         1001                 }
         1002                 rendered = b.Bytes()
         1003 
         1004                 if parseInfo.hasNonMarkdownShortcode {
         1005                         var hasShortcodeVariants bool
         1006 
         1007                         tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
         1008                                 if token == tocShortcodePlaceholder {
         1009                                         toc, err := c.contentToC(ctx)
         1010                                         if err != nil {
         1011                                                 return nil, err
         1012                                         }
         1013                                         // The Page's TableOfContents was accessed in a shortcode.
         1014                                         return []byte(toc.tableOfContentsHTML), nil
         1015                                 }
         1016                                 renderer, found := placeholders[token]
         1017                                 if found {
         1018                                         repl, more, err := renderer.renderShortcode(ctx)
         1019                                         if err != nil {
         1020                                                 return nil, err
         1021                                         }
         1022                                         hasShortcodeVariants = hasShortcodeVariants || more
         1023                                         return repl, nil
         1024                                 }
         1025                                 // This should not happen.
         1026                                 return nil, fmt.Errorf("unknown shortcode token %q", token)
         1027                         }
         1028 
         1029                         rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
         1030                         if err != nil {
         1031                                 return "", err
         1032                         }
         1033                         if hasShortcodeVariants {
         1034                                 pco.po.p.incrPageOutputTemplateVariation()
         1035                         }
         1036                 }
         1037 
         1038                 // We need a consolidated view in $page.HasShortcode
         1039                 pco.po.p.m.content.shortcodeState.transferNames(s)
         1040 
         1041         } else {
         1042                 c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
         1043                 if err != nil {
         1044                         return "", pco.po.p.wrapError(err)
         1045                 }
         1046 
         1047                 rendered = c.Bytes()
         1048         }
         1049 
         1050         if opts.Display == "inline" {
         1051                 markup := pco.po.p.m.pageConfig.Content.Markup
         1052                 if opts.Markup != "" {
         1053                         markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup)
         1054                 }
         1055                 rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup)
         1056         }
         1057 
         1058         return template.HTML(string(rendered)), nil
         1059 }
         1060 
         1061 func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) {
         1062         ctx = c.prepareContext(ctx)
         1063 
         1064         pco := c.pco
         1065         content := pco.po.p.m.content
         1066 
         1067         source, err := content.pi.contentSource(content)
         1068         if err != nil {
         1069                 return "", err
         1070         }
         1071         ct, err := c.contentToC(ctx)
         1072         if err != nil {
         1073                 return "", err
         1074         }
         1075 
         1076         var insertPlaceholders bool
         1077         var hasVariants bool
         1078         cb := setGetContentCallbackInContext.Get(ctx)
         1079         if cb != nil {
         1080                 insertPlaceholders = true
         1081         }
         1082         cc := make([]byte, 0, len(source)+(len(source)/10))
         1083         for _, it := range content.pi.itemsStep2 {
         1084                 switch v := it.(type) {
         1085                 case pageparser.Item:
         1086                         cc = append(cc, source[v.Pos():v.Pos()+len(v.Val(source))]...)
         1087                 case pageContentReplacement:
         1088                         // Ignore.
         1089                 case *shortcode:
         1090                         if !insertPlaceholders || !v.insertPlaceholder() {
         1091                                 // Insert the rendered shortcode.
         1092                                 renderedShortcode, found := ct.contentPlaceholders[v.placeholder]
         1093                                 if !found {
         1094                                         // This should never happen.
         1095                                         panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
         1096                                 }
         1097 
         1098                                 b, more, err := renderedShortcode.renderShortcode(ctx)
         1099                                 if err != nil {
         1100                                         return "", fmt.Errorf("failed to render shortcode: %w", err)
         1101                                 }
         1102                                 hasVariants = hasVariants || more
         1103                                 cc = append(cc, []byte(b)...)
         1104 
         1105                         } else {
         1106                                 // Insert the placeholder so we can insert the content after
         1107                                 // markdown processing.
         1108                                 cc = append(cc, []byte(v.placeholder)...)
         1109                         }
         1110                 default:
         1111                         panic(fmt.Sprintf("unknown item type %T", it))
         1112                 }
         1113         }
         1114 
         1115         if hasVariants {
         1116                 pco.po.p.incrPageOutputTemplateVariation()
         1117         }
         1118 
         1119         if cb != nil {
         1120                 cb(pco, ct)
         1121         }
         1122 
         1123         if tpl.Context.IsInGoldmark.Get(ctx) {
         1124                 // This content will be parsed and rendered by Goldmark.
         1125                 // Wrap it in a special Hugo markup to assign the correct Page from
         1126                 // the stack.
         1127                 return template.HTML(hugocontext.Wrap(cc, pco.po.p.pid)), nil
         1128         }
         1129 
         1130         return helpers.BytesToHTML(cc), nil
         1131 }
         1132 
         1133 func (c *cachedContentScope) Plain(ctx context.Context) string {
         1134         ctx = c.prepareContext(ctx)
         1135         return c.mustContentPlain(ctx).plain
         1136 }
         1137 
         1138 func (c *cachedContentScope) PlainWords(ctx context.Context) []string {
         1139         ctx = c.prepareContext(ctx)
         1140         return c.mustContentPlain(ctx).plainWords
         1141 }
         1142 
         1143 func (c *cachedContentScope) WordCount(ctx context.Context) int {
         1144         ctx = c.prepareContext(ctx)
         1145         return c.mustContentPlain(ctx).wordCount
         1146 }
         1147 
         1148 func (c *cachedContentScope) FuzzyWordCount(ctx context.Context) int {
         1149         ctx = c.prepareContext(ctx)
         1150         return c.mustContentPlain(ctx).fuzzyWordCount
         1151 }
         1152 
         1153 func (c *cachedContentScope) ReadingTime(ctx context.Context) int {
         1154         ctx = c.prepareContext(ctx)
         1155         return c.mustContentPlain(ctx).readingTime
         1156 }
         1157 
         1158 func (c *cachedContentScope) Len(ctx context.Context) int {
         1159         ctx = c.prepareContext(ctx)
         1160         return len(c.mustContentRendered(ctx).content)
         1161 }
         1162 
         1163 func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments {
         1164         ctx = c.prepareContext(ctx)
         1165         toc := c.mustContentToC(ctx).tableOfContents
         1166         if toc == nil {
         1167                 return nil
         1168         }
         1169         return toc
         1170 }
         1171 
         1172 func (c *cachedContentScope) fragmentsHTML(ctx context.Context) template.HTML {
         1173         ctx = c.prepareContext(ctx)
         1174         return c.mustContentToC(ctx).tableOfContentsHTML
         1175 }
         1176 
         1177 func (c *cachedContentScope) mustContentPlain(ctx context.Context) contentPlainPlainWords {
         1178         r, err := c.contentPlain(ctx)
         1179         if err != nil {
         1180                 c.pco.fail(err)
         1181         }
         1182         return r
         1183 }
         1184 
         1185 func (c *cachedContentScope) mustContentRendered(ctx context.Context) contentSummary {
         1186         r, err := c.contentRendered(ctx)
         1187         if err != nil {
         1188                 c.pco.fail(err)
         1189         }
         1190         return r
         1191 }