github.com/neohugo/neohugo@v0.123.8/hugolib/page__content.go (about) 1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "bytes" 18 "context" 19 "errors" 20 "fmt" 21 "html/template" 22 "io" 23 "strconv" 24 "strings" 25 "unicode/utf8" 26 27 "github.com/bep/logg" 28 "github.com/neohugo/neohugo/common/hcontext" 29 "github.com/neohugo/neohugo/common/herrors" 30 "github.com/neohugo/neohugo/common/hugio" 31 "github.com/neohugo/neohugo/helpers" 32 "github.com/neohugo/neohugo/identity" 33 "github.com/neohugo/neohugo/markup/converter" 34 "github.com/neohugo/neohugo/markup/tableofcontents" 35 "github.com/neohugo/neohugo/parser/metadecoders" 36 "github.com/neohugo/neohugo/parser/pageparser" 37 "github.com/neohugo/neohugo/resources" 38 "github.com/neohugo/neohugo/resources/resource" 39 "github.com/neohugo/neohugo/tpl" 40 ) 41 42 const ( 43 internalSummaryDividerBase = "HUGOMORE42" 44 ) 45 46 var ( 47 internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase) 48 internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n") 49 ) 50 51 type pageContentReplacement struct { 52 val []byte 53 54 source pageparser.Item 55 } 56 57 func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) { 58 var openSource hugio.OpenReadSeekCloser 59 if m.f != nil { 60 meta := m.f.FileInfo().Meta() 61 openSource = func() (hugio.ReadSeekCloser, error) { 62 r, err := meta.Open() 63 if err != nil { 64 return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err) 65 } 66 return r, nil 67 } 68 } 69 70 if sourceKey == "" { 71 sourceKey = strconv.Itoa(int(pid)) 72 } 73 74 pi := &contentParseInfo{ 75 h: h, 76 pid: pid, 77 sourceKey: sourceKey, 78 openSource: openSource, 79 } 80 81 source, err := pi.contentSource(m) 82 if err != nil { 83 return nil, err 84 } 85 86 items, err := pageparser.ParseBytes( 87 source, 88 pageparser.Config{}, 89 ) 90 if err != nil { 91 return nil, err 92 } 93 94 pi.itemsStep1 = items 95 96 if err := pi.mapFrontMatter(source); err != nil { 97 return nil, err 98 } 99 100 return pi, nil 101 } 102 103 func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) { 104 var filename string 105 if m.f != nil { 106 filename = m.f.Filename() 107 } 108 109 c := &cachedContent{ 110 pm: m.s.pageMap, 111 StaleInfo: m, 112 shortcodeState: newShortcodeHandler(filename, m.s), 113 pi: pi, 114 enableEmoji: m.s.conf.EnableEmoji, 115 } 116 117 source, err := c.pi.contentSource(m) 118 if err != nil { 119 return nil, err 120 } 121 122 if err := c.parseContentFile(source); err != nil { 123 return nil, err 124 } 125 126 return c, nil 127 } 128 129 type cachedContent struct { 130 pm *pageMap 131 132 resource.StaleInfo 133 134 shortcodeState *shortcodeHandler 135 136 // Parsed content. 137 pi *contentParseInfo 138 139 enableEmoji bool 140 } 141 142 type contentParseInfo struct { 143 h *HugoSites 144 145 pid uint64 146 sourceKey string 147 148 // The source bytes. 149 openSource hugio.OpenReadSeekCloser 150 151 frontMatter map[string]any 152 153 // Whether the parsed content contains a summary separator. 154 hasSummaryDivider bool 155 156 // Whether there are more content after the summary divider. 157 summaryTruncated bool 158 159 // Returns the position in bytes after any front matter. 160 posMainContent int 161 162 // Indicates whether we must do placeholder replacements. 163 hasNonMarkdownShortcode bool 164 165 // Items from the page parser. 166 // These maps directly to the source 167 itemsStep1 pageparser.Items 168 169 // *shortcode, pageContentReplacement or pageparser.Item 170 itemsStep2 []any 171 } 172 173 func (p *contentParseInfo) AddBytes(item pageparser.Item) { 174 p.itemsStep2 = append(p.itemsStep2, item) 175 } 176 177 func (p *contentParseInfo) AddReplacement(val []byte, source pageparser.Item) { 178 p.itemsStep2 = append(p.itemsStep2, pageContentReplacement{val: val, source: source}) 179 } 180 181 func (p *contentParseInfo) AddShortcode(s *shortcode) { 182 p.itemsStep2 = append(p.itemsStep2, s) 183 if s.insertPlaceholder() { 184 p.hasNonMarkdownShortcode = true 185 } 186 } 187 188 // contentToRenderForItems returns the content to be processed by Goldmark or similar. 189 func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) { 190 var hasVariants bool 191 c := make([]byte, 0, len(source)+(len(source)/10)) 192 193 for _, it := range pi.itemsStep2 { 194 switch v := it.(type) { 195 case pageparser.Item: 196 c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) 197 case pageContentReplacement: 198 c = append(c, v.val...) 199 case *shortcode: 200 if !v.insertPlaceholder() { 201 // Insert the rendered shortcode. 202 renderedShortcode, found := renderedShortcodes[v.placeholder] 203 if !found { 204 // This should never happen. 205 panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) 206 } 207 208 b, more, err := renderedShortcode.renderShortcode(ctx) 209 if err != nil { 210 return nil, false, fmt.Errorf("failed to render shortcode: %w", err) 211 } 212 hasVariants = hasVariants || more 213 c = append(c, []byte(b)...) 214 215 } else { 216 // Insert the placeholder so we can insert the content after 217 // markdown processing. 218 c = append(c, []byte(v.placeholder)...) 219 } 220 default: 221 panic(fmt.Sprintf("unknown item type %T", it)) 222 } 223 } 224 225 return c, hasVariants, nil 226 } 227 228 func (c *cachedContent) IsZero() bool { 229 return len(c.pi.itemsStep2) == 0 230 } 231 232 func (c *cachedContent) parseContentFile(source []byte) error { 233 if source == nil || c.pi.openSource == nil { 234 return nil 235 } 236 237 return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState) 238 } 239 240 func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error { 241 if c.frontMatter != nil { 242 return nil 243 } 244 245 f := pageparser.FormatFromFrontMatterType(it.Type) 246 var err error 247 c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f) 248 if err != nil { 249 if fe, ok := err.(herrors.FileError); ok { 250 pos := fe.Position() 251 252 // Offset the starting position of front matter. 253 offset := iter.LineNumber(source) - 1 254 if f == metadecoders.YAML { 255 offset -= 1 256 } 257 pos.LineNumber += offset 258 259 fe.UpdatePosition(pos) // nolint 260 fe.SetFilename("") // nolint It will be set later. 261 262 return fe 263 } else { 264 return err 265 } 266 } 267 268 return nil 269 } 270 271 func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error { 272 if fe, ok := err.(herrors.FileError); ok { 273 return fe 274 } 275 276 pos := posFromInput("", source, i.Pos()) 277 278 return herrors.NewFileErrorFromPos(err, pos) 279 } 280 281 func (rn *contentParseInfo) mapFrontMatter(source []byte) error { 282 if len(rn.itemsStep1) == 0 { 283 return nil 284 } 285 iter := pageparser.NewIterator(rn.itemsStep1) 286 287 Loop: 288 for { 289 it := iter.Next() 290 switch { 291 case it.IsFrontMatter(): 292 if err := rn.parseFrontMatter(it, iter, source); err != nil { 293 return err 294 } 295 next := iter.Peek() 296 if !next.IsDone() { 297 rn.posMainContent = next.Pos() 298 } 299 // Done. 300 break Loop 301 case it.IsEOF(): 302 break Loop 303 case it.IsError(): 304 return rn.failMap(source, it.Err, it) 305 default: 306 307 } 308 } 309 310 return nil 311 } 312 313 func (rn *contentParseInfo) mapItemsAfterFrontMatter( 314 source []byte, 315 s *shortcodeHandler, 316 ) error { 317 if len(rn.itemsStep1) == 0 { 318 return nil 319 } 320 321 fail := func(err error, i pageparser.Item) error { 322 if fe, ok := err.(herrors.FileError); ok { 323 return fe 324 } 325 326 pos := posFromInput("", source, i.Pos()) 327 328 return herrors.NewFileErrorFromPos(err, pos) 329 } 330 331 iter := pageparser.NewIterator(rn.itemsStep1) 332 333 // the parser is guaranteed to return items in proper order or fail, so … 334 // … it's safe to keep some "global" state 335 var ordinal int 336 337 Loop: 338 for { 339 it := iter.Next() 340 341 switch { 342 case it.Type == pageparser.TypeIgnore: 343 case it.IsFrontMatter(): 344 // Ignore. 345 case it.Type == pageparser.TypeLeadSummaryDivider: 346 posBody := -1 347 f := func(item pageparser.Item) bool { 348 if posBody == -1 && !item.IsDone() { 349 posBody = item.Pos() 350 } 351 352 if item.IsNonWhitespace(source) { 353 rn.summaryTruncated = true 354 355 // Done 356 return false 357 } 358 return true 359 } 360 iter.PeekWalk(f) 361 362 rn.hasSummaryDivider = true 363 364 // The content may be rendered by Goldmark or similar, 365 // and we need to track the summary. 366 rn.AddReplacement(internalSummaryDividerPre, it) 367 368 // Handle shortcode 369 case it.IsLeftShortcodeDelim(): 370 // let extractShortcode handle left delim (will do so recursively) 371 iter.Backup() 372 373 currShortcode, err := s.extractShortcode(ordinal, 0, source, iter) 374 if err != nil { 375 return fail(err, it) 376 } 377 378 currShortcode.pos = it.Pos() 379 currShortcode.length = iter.Current().Pos() - it.Pos() 380 if currShortcode.placeholder == "" { 381 currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, currShortcode.ordinal) 382 } 383 384 if currShortcode.name != "" { 385 s.addName(currShortcode.name) 386 } 387 388 if currShortcode.params == nil { 389 var s []string 390 currShortcode.params = s 391 } 392 393 currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, ordinal) 394 ordinal++ 395 s.shortcodes = append(s.shortcodes, currShortcode) 396 397 rn.AddShortcode(currShortcode) 398 399 case it.IsEOF(): 400 break Loop 401 case it.IsError(): 402 return fail(it.Err, it) 403 default: 404 rn.AddBytes(it) 405 } 406 } 407 408 return nil 409 } 410 411 func (c *cachedContent) mustSource() []byte { 412 source, err := c.pi.contentSource(c) 413 if err != nil { 414 panic(err) 415 } 416 return source 417 } 418 419 func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) { 420 key := c.sourceKey 421 v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) { 422 b, err := c.readSourceAll() 423 if err != nil { 424 return nil, err 425 } 426 427 return &resources.StaleValue[[]byte]{ 428 Value: b, 429 IsStaleFunc: func() bool { 430 return s.IsStale() 431 }, 432 }, nil 433 }) 434 if err != nil { 435 return nil, err 436 } 437 438 return v.Value, nil 439 } 440 441 func (c *contentParseInfo) readSourceAll() ([]byte, error) { 442 if c.openSource == nil { 443 return []byte{}, nil 444 } 445 r, err := c.openSource() 446 if err != nil { 447 return nil, err 448 } 449 defer r.Close() 450 451 return io.ReadAll(r) 452 } 453 454 type contentTableOfContents struct { 455 // For Goldmark we split Parse and Render. 456 astDoc any 457 458 tableOfContents *tableofcontents.Fragments 459 tableOfContentsHTML template.HTML 460 461 // Temporary storage of placeholders mapped to their content. 462 // These are shortcodes etc. Some of these will need to be replaced 463 // after any markup is rendered, so they share a common prefix. 464 contentPlaceholders map[string]shortcodeRenderer 465 466 contentToRender []byte 467 } 468 469 type contentSummary struct { 470 content template.HTML 471 summary template.HTML 472 summaryTruncated bool 473 } 474 475 type contentPlainPlainWords struct { 476 plain string 477 plainWords []string 478 479 summary template.HTML 480 summaryTruncated bool 481 482 wordCount int 483 fuzzyWordCount int 484 readingTime int 485 } 486 487 func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) { 488 ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal) 489 key := c.pi.sourceKey + "/" + cp.po.f.Name 490 versionv := cp.contentRenderedVersion 491 492 v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { 493 cp.po.p.s.Log.Trace(logg.StringFunc(func() string { 494 return fmt.Sprintln("contentRendered", key) 495 })) 496 497 cp.po.p.s.h.contentRenderCounter.Add(1) 498 cp.contentRendered = true 499 po := cp.po 500 501 ct, err := c.contentToC(ctx, cp) 502 if err != nil { 503 return nil, err 504 } 505 506 rs := &resources.StaleValue[contentSummary]{ 507 IsStaleFunc: func() bool { 508 return c.IsStale() || cp.contentRenderedVersion != versionv 509 }, 510 } 511 512 if len(c.pi.itemsStep2) == 0 { 513 // Nothing to do. 514 return rs, nil 515 } 516 517 var b []byte 518 519 if ct.astDoc != nil { 520 // The content is parsed, but not rendered. 521 r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc) 522 if err != nil { 523 return nil, err 524 } 525 if !ok { 526 return nil, errors.New("invalid state: astDoc is set but RenderContent returned false") 527 } 528 529 b = r.Bytes() 530 531 } else { 532 // Copy the content to be rendered. 533 b = make([]byte, len(ct.contentToRender)) 534 copy(b, ct.contentToRender) 535 } 536 537 // There are one or more replacement tokens to be replaced. 538 var hasShortcodeVariants bool 539 tokenHandler := func(ctx context.Context, token string) ([]byte, error) { 540 if token == tocShortcodePlaceholder { 541 return []byte(ct.tableOfContentsHTML), nil 542 } 543 renderer, found := ct.contentPlaceholders[token] 544 if found { 545 repl, more, err := renderer.renderShortcode(ctx) 546 if err != nil { 547 return nil, err 548 } 549 hasShortcodeVariants = hasShortcodeVariants || more 550 return repl, nil 551 } 552 // This should never happen. 553 panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders))) 554 } 555 556 b, err = expandShortcodeTokens(ctx, b, tokenHandler) 557 if err != nil { 558 return nil, err 559 } 560 if hasShortcodeVariants { 561 cp.po.p.pageOutputTemplateVariationsState.Add(1) 562 } 563 564 var result contentSummary // hasVariants bool 565 566 if c.pi.hasSummaryDivider { 567 isHTML := cp.po.p.m.pageConfig.Markup == "html" 568 if isHTML { 569 // Use the summary sections as provided by the user. 570 i := bytes.Index(b, internalSummaryDividerPre) 571 result.summary = helpers.BytesToHTML(b[:i]) 572 b = b[i+len(internalSummaryDividerPre):] 573 574 } else { 575 summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b) 576 if err != nil { 577 cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err) 578 } else { 579 b = content 580 result.summary = helpers.BytesToHTML(summary) 581 } 582 } 583 result.summaryTruncated = c.pi.summaryTruncated 584 } 585 result.content = helpers.BytesToHTML(b) 586 rs.Value = result 587 588 return rs, nil 589 }) 590 if err != nil { 591 return contentSummary{}, cp.po.p.wrapError(err) 592 } 593 594 return v.Value, nil 595 } 596 597 func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutput) contentTableOfContents { 598 ct, err := c.contentToC(ctx, cp) 599 if err != nil { 600 panic(err) 601 } 602 return ct 603 } 604 605 var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback") 606 607 func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) { 608 key := c.pi.sourceKey + "/" + cp.po.f.Name 609 versionv := cp.contentRenderedVersion 610 611 v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { 612 source, err := c.pi.contentSource(c) 613 if err != nil { 614 return nil, err 615 } 616 617 var ct contentTableOfContents 618 if err := cp.initRenderHooks(); err != nil { 619 return nil, err 620 } 621 f := cp.po.f 622 po := cp.po 623 p := po.p 624 ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false) 625 if err != nil { 626 return nil, err 627 } 628 629 // Callback called from above (e.g. in .RenderString) 630 ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) { 631 // Merge content placeholders 632 for k, v := range ct2.contentPlaceholders { 633 ct.contentPlaceholders[k] = v 634 } 635 636 if p.s.conf.Internal.Watch { 637 for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes { 638 for _, templ := range s.templs { 639 cp.trackDependency(templ.(identity.IdentityProvider)) 640 } 641 } 642 } 643 644 // Transfer shortcode names so HasShortcode works for shortcodes from included pages. 645 cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState) 646 if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 { 647 cp.po.p.pageOutputTemplateVariationsState.Add(1) 648 } 649 } 650 651 ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) 652 653 var hasVariants bool 654 ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders) 655 if err != nil { 656 return nil, err 657 } 658 659 if hasVariants { 660 p.pageOutputTemplateVariationsState.Add(1) 661 } 662 663 isHTML := cp.po.p.m.pageConfig.Markup == "html" 664 665 if !isHTML { 666 createAndSetToC := func(tocProvider converter.TableOfContentsProvider) { 667 cfg := p.s.ContentSpec.Converters.GetMarkupConfig() 668 ct.tableOfContents = tocProvider.TableOfContents() 669 ct.tableOfContentsHTML = template.HTML( 670 ct.tableOfContents.ToHTML( 671 cfg.TableOfContents.StartLevel, 672 cfg.TableOfContents.EndLevel, 673 cfg.TableOfContents.Ordered, 674 ), 675 ) 676 } 677 678 // If the converter supports doing the parsing separately, we do that. 679 parseResult, ok, err := po.contentRenderer.ParseContent(ctx, ct.contentToRender) 680 if err != nil { 681 return nil, err 682 } 683 if ok { 684 // This is Goldmark. 685 // Store away the parse result for later use. 686 createAndSetToC(parseResult) 687 688 ct.astDoc = parseResult.Doc() 689 690 } else { 691 692 // This is Asciidoctor etc. 693 r, err := po.contentRenderer.ParseAndRenderContent(ctx, ct.contentToRender, true) 694 if err != nil { 695 return nil, err 696 } 697 698 ct.contentToRender = r.Bytes() 699 700 if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { 701 createAndSetToC(tocProvider) 702 } else { 703 tmpContent, tmpTableOfContents := helpers.ExtractTOC(ct.contentToRender) 704 ct.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents) 705 ct.tableOfContents = tableofcontents.Empty 706 ct.contentToRender = tmpContent 707 } 708 } 709 } 710 711 return &resources.StaleValue[contentTableOfContents]{ 712 Value: ct, 713 IsStaleFunc: func() bool { 714 return c.IsStale() || cp.contentRenderedVersion != versionv 715 }, 716 }, nil 717 }) 718 if err != nil { 719 return contentTableOfContents{}, err 720 } 721 722 return v.Value, nil 723 } 724 725 func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) { 726 key := c.pi.sourceKey + "/" + cp.po.f.Name 727 728 versionv := cp.contentRenderedVersion 729 730 v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) { 731 var result contentPlainPlainWords 732 rs := &resources.StaleValue[contentPlainPlainWords]{ 733 IsStaleFunc: func() bool { 734 return c.IsStale() || cp.contentRenderedVersion != versionv 735 }, 736 } 737 738 rendered, err := c.contentRendered(ctx, cp) 739 if err != nil { 740 return nil, err 741 } 742 743 result.plain = tpl.StripHTML(string(rendered.content)) 744 result.plainWords = strings.Fields(result.plain) 745 746 isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage 747 748 if isCJKLanguage { 749 result.wordCount = 0 750 for _, word := range result.plainWords { 751 runeCount := utf8.RuneCountInString(word) 752 if len(word) == runeCount { 753 result.wordCount++ 754 } else { 755 result.wordCount += runeCount 756 } 757 } 758 } else { 759 result.wordCount = helpers.TotalWords(result.plain) 760 } 761 762 // TODO(bep) is set in a test. Fix that. 763 if result.fuzzyWordCount == 0 { 764 result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100 765 } 766 767 if isCJKLanguage { 768 result.readingTime = (result.wordCount + 500) / 501 769 } else { 770 result.readingTime = (result.wordCount + 212) / 213 771 } 772 773 if rendered.summary != "" { 774 result.summary = rendered.summary 775 result.summaryTruncated = rendered.summaryTruncated 776 } else if cp.po.p.m.pageConfig.Summary != "" { 777 b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false) 778 if err != nil { 779 return nil, err 780 } 781 html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes()) 782 result.summary = helpers.BytesToHTML(html) 783 } else { 784 var summary string 785 var truncated bool 786 if isCJKLanguage { 787 summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsByRune(result.plainWords) 788 } else { 789 summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsToWholeSentence(result.plain) 790 } 791 result.summary = template.HTML(summary) 792 result.summaryTruncated = truncated 793 } 794 795 rs.Value = result 796 797 return rs, nil 798 }) 799 if err != nil { 800 if herrors.IsTimeoutError(err) { 801 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) 802 } 803 return contentPlainPlainWords{}, err 804 } 805 return v.Value, nil 806 }