github.com/neohugo/neohugo@v0.123.8/hugolib/page.go (about) 1 // Copyright 2024 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 "fmt" 19 "strconv" 20 "sync" 21 "sync/atomic" 22 23 "github.com/neohugo/neohugo/hugofs" 24 "github.com/neohugo/neohugo/hugolib/doctree" 25 "github.com/neohugo/neohugo/identity" 26 "github.com/neohugo/neohugo/media" 27 "github.com/neohugo/neohugo/output" 28 "github.com/neohugo/neohugo/output/layouts" 29 "github.com/neohugo/neohugo/related" 30 "github.com/spf13/afero" 31 32 "github.com/neohugo/neohugo/markup/converter" 33 "github.com/neohugo/neohugo/markup/tableofcontents" 34 35 "github.com/neohugo/neohugo/tpl" 36 37 "github.com/neohugo/neohugo/common/herrors" 38 "github.com/neohugo/neohugo/common/maps" 39 40 "github.com/neohugo/neohugo/source" 41 42 "github.com/neohugo/neohugo/common/collections" 43 "github.com/neohugo/neohugo/common/text" 44 "github.com/neohugo/neohugo/resources/kinds" 45 "github.com/neohugo/neohugo/resources/page" 46 "github.com/neohugo/neohugo/resources/resource" 47 ) 48 49 var ( 50 _ page.Page = (*pageState)(nil) 51 _ collections.Grouper = (*pageState)(nil) 52 _ collections.Slicer = (*pageState)(nil) 53 _ identity.DependencyManagerScopedProvider = (*pageState)(nil) 54 _ contentNodeI = (*pageState)(nil) 55 _ pageContext = (*pageState)(nil) 56 ) 57 58 var ( 59 pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType) 60 nopPageOutput = &pageOutput{ 61 pagePerOutputProviders: nopPagePerOutput, 62 ContentProvider: page.NopPage, 63 } 64 ) 65 66 // pageContext provides contextual information about this page, for error 67 // logging and similar. 68 type pageContext interface { 69 posOffset(offset int) text.Position 70 wrapError(err error) error 71 getContentConverter() converter.Converter 72 } 73 74 type pageSiteAdapter struct { 75 p page.Page 76 s *Site 77 } 78 79 func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) { 80 p, err := pa.s.getPage(pa.p, ref) 81 82 if p == nil { 83 // The nil struct has meaning in some situations, mostly to avoid breaking 84 // existing sites doing $nilpage.IsDescendant($p), which will always return 85 // false. 86 p = page.NilPage 87 } 88 return p, err 89 } 90 91 type pageState struct { 92 // Incremented for each new page created. 93 // Note that this will change between builds for a given Page. 94 pid uint64 95 96 // This slice will be of same length as the number of global slice of output 97 // formats (for all sites). 98 pageOutputs []*pageOutput 99 100 // Used to determine if we can reuse content across output formats. 101 pageOutputTemplateVariationsState *atomic.Uint32 102 103 // This will be shifted out when we start to render a new output format. 104 pageOutputIdx int 105 *pageOutput 106 107 // Common for all output formats. 108 *pageCommon 109 110 resource.Staler 111 dependencyManager identity.Manager 112 resourcesPublishInit *sync.Once 113 } 114 115 func (p *pageState) IdentifierBase() string { 116 return p.Path() 117 } 118 119 func (p *pageState) GetIdentity() identity.Identity { 120 return p 121 } 122 123 func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool { 124 return f(p) 125 } 126 127 func (p *pageState) GetDependencyManager() identity.Manager { 128 return p.dependencyManager 129 } 130 131 func (p *pageState) GetDependencyManagerForScope(scope int) identity.Manager { 132 switch scope { 133 case pageDependencyScopeDefault: 134 return p.dependencyManagerOutput 135 case pageDependencyScopeGlobal: 136 return p.dependencyManager 137 default: 138 return identity.NopManager 139 } 140 } 141 142 func (p *pageState) Key() string { 143 return "page-" + strconv.FormatUint(p.pid, 10) 144 } 145 146 func (p *pageState) resetBuildState() { 147 p.Scratcher = maps.NewScratcher() 148 } 149 150 func (p *pageState) reusePageOutputContent() bool { 151 return p.pageOutputTemplateVariationsState.Load() == 1 152 } 153 154 func (po *pageState) isRenderedAny() bool { 155 for _, o := range po.pageOutputs { 156 if o.isRendered() { 157 return true 158 } 159 } 160 return false 161 } 162 163 func (p *pageState) isContentNodeBranch() bool { 164 return p.IsNode() 165 } 166 167 func (p *pageState) Err() resource.ResourceError { 168 return nil 169 } 170 171 // Eq returns whether the current page equals the given page. 172 // This is what's invoked when doing `{{ if eq $page $otherPage }}` 173 func (p *pageState) Eq(other any) bool { 174 pp, err := unwrapPage(other) 175 if err != nil { 176 return false 177 } 178 179 return p == pp 180 } 181 182 func (p *pageState) HeadingsFiltered(context.Context) tableofcontents.Headings { 183 return nil 184 } 185 186 type pageHeadingsFiltered struct { 187 *pageState 188 headings tableofcontents.Headings 189 } 190 191 func (p *pageHeadingsFiltered) HeadingsFiltered(context.Context) tableofcontents.Headings { 192 return p.headings 193 } 194 195 func (p *pageHeadingsFiltered) page() page.Page { 196 return p.pageState 197 } 198 199 // For internal use by the related content feature. 200 func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { 201 r, err := p.m.content.contentToC(ctx, p.pageOutput.pco) 202 if err != nil { 203 panic(err) 204 } 205 headings := r.tableOfContents.Headings.FilterBy(fn) 206 return &pageHeadingsFiltered{ 207 pageState: p, 208 headings: headings, 209 } 210 } 211 212 func (p *pageState) GitInfo() source.GitInfo { 213 return p.gitInfo 214 } 215 216 func (p *pageState) CodeOwners() []string { 217 return p.codeowners 218 } 219 220 // GetTerms gets the terms defined on this page in the given taxonomy. 221 // The pages returned will be ordered according to the front matter. 222 func (p *pageState) GetTerms(taxonomy string) page.Pages { 223 return p.s.pageMap.getTermsForPageInTaxonomy(p.Path(), taxonomy) 224 } 225 226 func (p *pageState) MarshalJSON() ([]byte, error) { 227 return page.MarshalPageToJSON(p) 228 } 229 230 func (p *pageState) RegularPagesRecursive() page.Pages { 231 switch p.Kind() { 232 case kinds.KindSection, kinds.KindHome: 233 return p.s.pageMap.getPagesInSection( 234 pageMapQueryPagesInSection{ 235 pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ 236 Path: p.Path(), 237 Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), 238 }, 239 Recursive: true, 240 }, 241 ) 242 default: 243 return p.RegularPages() 244 } 245 } 246 247 func (p *pageState) PagesRecursive() page.Pages { 248 return nil 249 } 250 251 func (p *pageState) RegularPages() page.Pages { 252 switch p.Kind() { 253 case kinds.KindPage: 254 case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy: 255 return p.s.pageMap.getPagesInSection( 256 pageMapQueryPagesInSection{ 257 pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ 258 Path: p.Path(), 259 Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), 260 }, 261 }, 262 ) 263 case kinds.KindTerm: 264 return p.s.pageMap.getPagesWithTerm( 265 pageMapQueryPagesBelowPath{ 266 Path: p.Path(), 267 Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), 268 }, 269 ) 270 default: 271 return p.s.RegularPages() 272 } 273 return nil 274 } 275 276 func (p *pageState) Pages() page.Pages { 277 switch p.Kind() { 278 case kinds.KindPage: 279 case kinds.KindSection, kinds.KindHome: 280 return p.s.pageMap.getPagesInSection( 281 pageMapQueryPagesInSection{ 282 pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ 283 Path: p.Path(), 284 KeyPart: "page-section", 285 Include: pagePredicates.ShouldListLocal.And( 286 pagePredicates.KindPage.Or(pagePredicates.KindSection), 287 ), 288 }, 289 }, 290 ) 291 case kinds.KindTerm: 292 return p.s.pageMap.getPagesWithTerm( 293 pageMapQueryPagesBelowPath{ 294 Path: p.Path(), 295 }, 296 ) 297 case kinds.KindTaxonomy: 298 return p.s.pageMap.getPagesInSection( 299 pageMapQueryPagesInSection{ 300 pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ 301 Path: p.Path(), 302 KeyPart: "term", 303 Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm), 304 }, 305 Recursive: true, 306 }, 307 ) 308 default: 309 return p.s.Pages() 310 } 311 return nil 312 } 313 314 // RawContent returns the un-rendered source content without 315 // any leading front matter. 316 func (p *pageState) RawContent() string { 317 if p.m.content.pi.itemsStep2 == nil { 318 return "" 319 } 320 start := p.m.content.pi.posMainContent 321 if start == -1 { 322 start = 0 323 } 324 source, err := p.m.content.pi.contentSource(p.m.content) 325 if err != nil { 326 panic(err) 327 } 328 return string(source[start:]) 329 } 330 331 func (p *pageState) Resources() resource.Resources { 332 return p.s.pageMap.getOrCreateResourcesForPage(p) 333 } 334 335 func (p *pageState) HasShortcode(name string) bool { 336 if p.m.content.shortcodeState == nil { 337 return false 338 } 339 340 return p.m.content.shortcodeState.hasName(name) 341 } 342 343 func (p *pageState) Site() page.Site { 344 return p.sWrapped 345 } 346 347 func (p *pageState) String() string { 348 return fmt.Sprintf("Page(%s)", p.Path()) 349 } 350 351 // IsTranslated returns whether this content file is translated to 352 // other language(s). 353 func (p *pageState) IsTranslated() bool { 354 return len(p.Translations()) > 0 355 } 356 357 // TranslationKey returns the key used to identify a translation of this content. 358 func (p *pageState) TranslationKey() string { 359 if p.m.pageConfig.TranslationKey != "" { 360 return p.m.pageConfig.TranslationKey 361 } 362 return p.Path() 363 } 364 365 // AllTranslations returns all translations, including the current Page. 366 func (p *pageState) AllTranslations() page.Pages { 367 key := p.Path() + "/" + "translations-all" 368 pages, err := p.s.pageMap.getOrCreatePagesFromCache(key, func(string) (page.Pages, error) { 369 if p.m.pageConfig.TranslationKey != "" { 370 // translationKey set by user. 371 pas, _ := p.s.h.translationKeyPages.Get(p.m.pageConfig.TranslationKey) 372 pasc := make(page.Pages, len(pas)) 373 copy(pasc, pas) 374 page.SortByLanguage(pasc) 375 return pasc, nil 376 } 377 var pas page.Pages 378 p.s.pageMap.treePages.ForEeachInDimension(p.Path(), doctree.DimensionLanguage.Index(), 379 func(n contentNodeI) bool { 380 if n != nil { 381 pas = append(pas, n.(page.Page)) 382 } 383 return false 384 }, 385 ) 386 387 pas = pagePredicates.ShouldLink.Filter(pas) 388 page.SortByLanguage(pas) 389 return pas, nil 390 }) 391 if err != nil { 392 panic(err) 393 } 394 395 return pages 396 } 397 398 // Translations returns the translations excluding the current Page. 399 func (p *pageState) Translations() page.Pages { 400 key := p.Path() + "/" + "translations" 401 pages, err := p.s.pageMap.getOrCreatePagesFromCache(key, func(string) (page.Pages, error) { 402 var pas page.Pages 403 for _, pp := range p.AllTranslations() { 404 if !pp.Eq(p) { 405 pas = append(pas, pp) 406 } 407 } 408 return pas, nil 409 }) 410 if err != nil { 411 panic(err) 412 } 413 return pages 414 } 415 416 func (ps *pageState) initCommonProviders(pp pagePaths) error { 417 if ps.IsPage() { 418 ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext} 419 ps.posNextPrevSection = &nextPrev{init: ps.s.init.prevNextInSection} 420 ps.InSectionPositioner = newPagePositionInSection(ps.posNextPrevSection) 421 ps.Positioner = newPagePosition(ps.posNextPrev) 422 } 423 424 ps.OutputFormatsProvider = pp 425 ps.targetPathDescriptor = pp.targetPathDescriptor 426 ps.RefProvider = newPageRef(ps) 427 ps.SitesProvider = ps.s 428 429 return nil 430 } 431 432 func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor { 433 p.layoutDescriptorInit.Do(func() { 434 var section string 435 sections := p.SectionsEntries() 436 437 switch p.Kind() { 438 case kinds.KindSection: 439 if len(sections) > 0 { 440 section = sections[0] 441 } 442 case kinds.KindTaxonomy, kinds.KindTerm: 443 444 if p.m.singular != "" { 445 section = p.m.singular 446 } else if len(sections) > 0 { 447 section = sections[0] 448 } 449 default: 450 } 451 452 p.layoutDescriptor = layouts.LayoutDescriptor{ 453 Kind: p.Kind(), 454 Type: p.Type(), 455 Lang: p.Language().Lang, 456 Layout: p.Layout(), 457 Section: section, 458 } 459 }) 460 461 return p.layoutDescriptor 462 } 463 464 func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) { 465 f := p.outputFormat() 466 467 d := p.getLayoutDescriptor() 468 469 if len(layouts) > 0 { 470 d.Layout = layouts[0] 471 d.LayoutOverride = true 472 } 473 474 return p.s.Tmpl().LookupLayout(d, f) 475 } 476 477 // Must be run after the site section tree etc. is built and ready. 478 func (p *pageState) initPage() error { 479 if _, err := p.init.Do(context.Background()); err != nil { 480 return err 481 } 482 return nil 483 } 484 485 func (p *pageState) renderResources() error { 486 var initErr error 487 488 p.resourcesPublishInit.Do(func() { 489 for _, r := range p.Resources() { 490 if _, ok := r.(page.Page); ok { 491 // Pages gets rendered with the owning page but we count them here. 492 p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) 493 continue 494 } 495 496 src, ok := r.(resource.Source) 497 if !ok { 498 initErr = fmt.Errorf("resource %T does not support resource.Source", src) 499 return 500 } 501 502 if err := src.Publish(); err != nil { 503 if !herrors.IsNotExist(err) { 504 p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err) 505 } 506 } else { 507 p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files) 508 } 509 } 510 }) 511 512 return initErr 513 } 514 515 func (p *pageState) AlternativeOutputFormats() page.OutputFormats { 516 f := p.outputFormat() 517 var o page.OutputFormats 518 for _, of := range p.OutputFormats() { 519 if of.Format.NotAlternative || of.Format.Name == f.Name { 520 continue 521 } 522 523 o = append(o, of) 524 } 525 return o 526 } 527 528 type renderStringOpts struct { 529 Display string 530 Markup string 531 } 532 533 var defaultRenderStringOpts = renderStringOpts{ 534 Display: "inline", 535 Markup: "", // Will inherit the page's value when not set. 536 } 537 538 func (p *pageMeta) wrapError(err error, sourceFs afero.Fs) error { 539 if err == nil { 540 panic("wrapError with nil") 541 } 542 543 if p.File() == nil { 544 // No more details to add. 545 return fmt.Errorf("%q: %w", p.Path(), err) 546 } 547 548 return hugofs.AddFileInfoToError(err, p.File().FileInfo(), sourceFs) 549 } 550 551 // wrapError adds some more context to the given error if possible/needed 552 func (p *pageState) wrapError(err error) error { 553 return p.m.wrapError(err, p.s.h.SourceFs) 554 } 555 556 func (p *pageState) getPageInfoForError() string { 557 s := fmt.Sprintf("kind: %q, path: %q", p.Kind(), p.Path()) 558 if p.File() != nil { 559 s += fmt.Sprintf(", file: %q", p.File().Filename()) 560 } 561 return s 562 } 563 564 func (p *pageState) getContentConverter() converter.Converter { 565 var err error 566 p.contentConverterInit.Do(func() { 567 markup := p.m.pageConfig.Markup 568 if markup == "html" { 569 // Only used for shortcode inner content. 570 markup = "markdown" 571 } 572 p.contentConverter, err = p.m.newContentConverter(p, markup) 573 }) 574 575 if err != nil { 576 p.s.Log.Errorln("Failed to create content converter:", err) 577 } 578 return p.contentConverter 579 } 580 581 func (p *pageState) errorf(err error, format string, a ...any) error { 582 if herrors.UnwrapFileError(err) != nil { 583 // More isn't always better. 584 return err 585 } 586 args := append([]any{p.Language().Lang, p.pathOrTitle()}, a...) 587 args = append(args, err) 588 format = "[%s] page %q: " + format + ": %w" 589 if err == nil { 590 return fmt.Errorf(format, args...) 591 } 592 return fmt.Errorf(format, args...) 593 } 594 595 func (p *pageState) outputFormat() (f output.Format) { 596 if p.pageOutput == nil { 597 panic("no pageOutput") 598 } 599 return p.pageOutput.f 600 } 601 602 func (p *pageState) parseError(err error, input []byte, offset int) error { 603 pos := posFromInput("", input, offset) 604 return herrors.NewFileErrorFromName(err, p.File().Filename()).UpdatePosition(pos) 605 } 606 607 func (p *pageState) pathOrTitle() string { 608 if p.File() != nil { 609 return p.File().Filename() 610 } 611 612 if p.Path() != "" { 613 return p.Path() 614 } 615 616 return p.Title() 617 } 618 619 func (p *pageState) posFromInput(input []byte, offset int) text.Position { 620 return posFromInput(p.pathOrTitle(), input, offset) 621 } 622 623 func (p *pageState) posOffset(offset int) text.Position { 624 return p.posFromInput(p.m.content.mustSource(), offset) 625 } 626 627 // shiftToOutputFormat is serialized. The output format idx refers to the 628 // full set of output formats for all sites. 629 // This is serialized. 630 func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { 631 if err := p.initPage(); err != nil { 632 return err 633 } 634 635 if len(p.pageOutputs) == 1 { 636 idx = 0 637 } 638 639 p.pageOutputIdx = idx 640 p.pageOutput = p.pageOutputs[idx] 641 if p.pageOutput == nil { 642 panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx)) 643 } 644 645 // Reset any built paginator. This will trigger when re-rendering pages in 646 // server mode. 647 if isRenderingSite && p.pageOutput.paginator != nil && p.pageOutput.paginator.current != nil { 648 p.pageOutput.paginator.reset() 649 } 650 651 if isRenderingSite { 652 cp := p.pageOutput.pco 653 if cp == nil && p.reusePageOutputContent() { 654 // Look for content to reuse. 655 for i := 0; i < len(p.pageOutputs); i++ { 656 if i == idx { 657 continue 658 } 659 po := p.pageOutputs[i] 660 661 if po.pco != nil { 662 cp = po.pco 663 break 664 } 665 } 666 } 667 668 if cp == nil { 669 var err error 670 cp, err = newPageContentOutput(p.pageOutput) 671 if err != nil { 672 return err 673 } 674 } 675 p.pageOutput.setContentProvider(cp) 676 } else { 677 // We attempt to assign pageContentOutputs while preparing each site 678 // for rendering and before rendering each site. This lets us share 679 // content between page outputs to conserve resources. But if a template 680 // unexpectedly calls a method of a ContentProvider that is not yet 681 // initialized, we assign a LazyContentProvider that performs the 682 // initialization just in time. 683 if lcp, ok := (p.pageOutput.ContentProvider.(*page.LazyContentProvider)); ok { 684 lcp.Reset() 685 } else { 686 lcp = page.NewLazyContentProvider(func() (page.OutputFormatContentProvider, error) { 687 cp, err := newPageContentOutput(p.pageOutput) 688 if err != nil { 689 return nil, err 690 } 691 return cp, nil 692 }) 693 p.pageOutput.contentRenderer = lcp 694 p.pageOutput.ContentProvider = lcp 695 p.pageOutput.PageRenderProvider = lcp 696 p.pageOutput.TableOfContentsProvider = lcp 697 } 698 } 699 700 return nil 701 } 702 703 var ( 704 _ page.Page = (*pageWithOrdinal)(nil) 705 _ collections.Order = (*pageWithOrdinal)(nil) 706 _ pageWrapper = (*pageWithOrdinal)(nil) 707 ) 708 709 type pageWithOrdinal struct { 710 ordinal int 711 *pageState 712 } 713 714 func (p pageWithOrdinal) Ordinal() int { 715 return p.ordinal 716 } 717 718 func (p pageWithOrdinal) page() page.Page { 719 return p.pageState 720 } 721 722 type pageWithWeight0 struct { 723 weight0 int 724 *pageState 725 } 726 727 func (p pageWithWeight0) Weight0() int { 728 return p.weight0 729 } 730 731 func (p pageWithWeight0) page() page.Page { 732 return p.pageState 733 }