github.com/jbramsden/hugo@v0.47.1/hugolib/page.go (about) 1 // Copyright 2018 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 "reflect" 22 "unicode" 23 24 "github.com/gohugoio/hugo/media" 25 26 "github.com/gohugoio/hugo/common/maps" 27 28 "github.com/gohugoio/hugo/langs" 29 30 "github.com/gohugoio/hugo/related" 31 32 "github.com/bep/gitmap" 33 34 "github.com/gohugoio/hugo/helpers" 35 "github.com/gohugoio/hugo/hugolib/pagemeta" 36 "github.com/gohugoio/hugo/resource" 37 38 "github.com/gohugoio/hugo/output" 39 "github.com/gohugoio/hugo/parser" 40 "github.com/mitchellh/mapstructure" 41 42 "html/template" 43 "io" 44 "path" 45 "path/filepath" 46 "regexp" 47 "runtime" 48 "strings" 49 "sync" 50 "time" 51 "unicode/utf8" 52 53 bp "github.com/gohugoio/hugo/bufferpool" 54 "github.com/gohugoio/hugo/compare" 55 "github.com/gohugoio/hugo/source" 56 "github.com/spf13/cast" 57 ) 58 59 var ( 60 cjk = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) 61 62 // This is all the kinds we can expect to find in .Site.Pages. 63 allKindsInPages = []string{KindPage, KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} 64 65 allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...) 66 67 // Assert that it implements the Eqer interface. 68 _ compare.Eqer = (*Page)(nil) 69 _ compare.Eqer = (*PageOutput)(nil) 70 71 // Assert that it implements the interface needed for related searches. 72 _ related.Document = (*Page)(nil) 73 ) 74 75 const ( 76 KindPage = "page" 77 78 // The rest are node types; home page, sections etc. 79 80 KindHome = "home" 81 KindSection = "section" 82 KindTaxonomy = "taxonomy" 83 KindTaxonomyTerm = "taxonomyTerm" 84 85 // Temporary state. 86 kindUnknown = "unknown" 87 88 // The following are (currently) temporary nodes, 89 // i.e. nodes we create just to render in isolation. 90 kindRSS = "RSS" 91 kindSitemap = "sitemap" 92 kindRobotsTXT = "robotsTXT" 93 kind404 = "404" 94 95 pageResourceType = "page" 96 ) 97 98 type Page struct { 99 *pageInit 100 *pageContentInit 101 102 // Kind is the discriminator that identifies the different page types 103 // in the different page collections. This can, as an example, be used 104 // to to filter regular pages, find sections etc. 105 // Kind will, for the pages available to the templates, be one of: 106 // page, home, section, taxonomy and taxonomyTerm. 107 // It is of string type to make it easy to reason about in 108 // the templates. 109 Kind string 110 111 // Since Hugo 0.18 we got rid of the Node type. So now all pages are ... 112 // pages (regular pages, home page, sections etc.). 113 // Sections etc. will have child pages. These were earlier placed in .Data.Pages, 114 // but can now be more intuitively also be fetched directly from .Pages. 115 // This collection will be nil for regular pages. 116 Pages Pages 117 118 // Since Hugo 0.32, a Page can have resources such as images and CSS associated 119 // with itself. The resource will typically be placed relative to the Page, 120 // but templates should use the links (Permalink and RelPermalink) 121 // provided by the Resource object. 122 Resources resource.Resources 123 124 // This is the raw front matter metadata that is going to be assigned to 125 // the Resources above. 126 resourcesMetadata []map[string]interface{} 127 128 // translations will contain references to this page in other language 129 // if available. 130 translations Pages 131 132 // A key that maps to translation(s) of this page. This value is fetched 133 // from the page front matter. 134 translationKey string 135 136 // Params contains configuration defined in the params section of page frontmatter. 137 params map[string]interface{} 138 139 // Content sections 140 contentv template.HTML 141 summary template.HTML 142 TableOfContents template.HTML 143 // Passed to the shortcodes 144 pageWithoutContent *PageWithoutContent 145 146 Aliases []string 147 148 Images []Image 149 Videos []Video 150 151 truncated bool 152 Draft bool 153 Status string 154 155 // PageMeta contains page stats such as word count etc. 156 PageMeta 157 158 // Markup contains the markup type for the content. 159 Markup string 160 161 extension string 162 contentType string 163 renderable bool 164 165 Layout string 166 167 // For npn-renderable pages (see IsRenderable), the content itself 168 // is used as template and the template name is stored here. 169 selfLayout string 170 171 linkTitle string 172 173 frontmatter []byte 174 175 // rawContent is the raw content read from the content file. 176 rawContent []byte 177 178 // workContent is a copy of rawContent that may be mutated during site build. 179 workContent []byte 180 181 // whether the content is in a CJK language. 182 isCJKLanguage bool 183 184 shortcodeState *shortcodeHandler 185 186 // the content stripped for HTML 187 plain string // TODO should be []byte 188 plainWords []string 189 190 // rendering configuration 191 renderingConfig *helpers.BlackFriday 192 193 // menus 194 pageMenus PageMenus 195 196 Source 197 198 Position `json:"-"` 199 200 GitInfo *gitmap.GitInfo 201 202 // This was added as part of getting the Nodes (taxonomies etc.) to work as 203 // Pages in Hugo 0.18. 204 // It is deliberately named similar to Section, but not exported (for now). 205 // We currently have only one level of section in Hugo, but the page can live 206 // any number of levels down the file path. 207 // To support taxonomies like /categories/hugo etc. we will need to keep track 208 // of that information in a general way. 209 // So, sections represents the path to the content, i.e. a content file or a 210 // virtual content file in the situations where a taxonomy or a section etc. 211 // isn't accomanied by one. 212 sections []string 213 214 // Will only be set for sections and regular pages. 215 parent *Page 216 217 // When we create paginator pages, we create a copy of the original, 218 // but keep track of it here. 219 origOnCopy *Page 220 221 // Will only be set for section pages and the home page. 222 subSections Pages 223 224 s *Site 225 226 // Pulled over from old Node. TODO(bep) reorg and group (embed) 227 228 Site *SiteInfo `json:"-"` 229 230 title string 231 Description string 232 Keywords []string 233 data map[string]interface{} 234 235 pagemeta.PageDates 236 237 Sitemap Sitemap 238 pagemeta.URLPath 239 frontMatterURL string 240 241 permalink string 242 relPermalink string 243 244 // relative target path without extension and any base path element 245 // from the baseURL or the language code. 246 // This is used to construct paths in the page resources. 247 relTargetPathBase string 248 // Is set to a forward slashed path if this is a Page resources living in a folder below its owner. 249 resourcePath string 250 251 // This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter. 252 // Being headless means that 253 // 1. The page itself is not rendered to disk 254 // 2. It is not available in .Site.Pages etc. 255 // 3. But you can get it via .Site.GetPage 256 headless bool 257 258 layoutDescriptor output.LayoutDescriptor 259 260 scratch *maps.Scratch 261 262 // It would be tempting to use the language set on the Site, but in they way we do 263 // multi-site processing, these values may differ during the initial page processing. 264 language *langs.Language 265 266 lang string 267 268 // When in Fast Render Mode, we only render a sub set of the pages, i.e. the 269 // pages the user is working on. There are, however, situations where we need to 270 // signal other pages to be rendered. 271 forceRender bool 272 273 // The output formats this page will be rendered to. 274 outputFormats output.Formats 275 276 // This is the PageOutput that represents the first item in outputFormats. 277 // Use with care, as there are potential for inifinite loops. 278 mainPageOutput *PageOutput 279 280 targetPathDescriptorPrototype *targetPathDescriptor 281 } 282 283 func stackTrace(length int) string { 284 trace := make([]byte, length) 285 runtime.Stack(trace, true) 286 return string(trace) 287 } 288 289 func (p *Page) Data() interface{} { 290 return p.data 291 } 292 293 func (p *Page) initContent() { 294 295 p.contentInit.Do(func() { 296 // This careful dance is here to protect against circular loops in shortcode/content 297 // constructs. 298 // TODO(bep) context vs the remote shortcodes 299 ctx, cancel := context.WithTimeout(context.Background(), p.s.Timeout) 300 defer cancel() 301 c := make(chan error, 1) 302 303 go func() { 304 var err error 305 p.contentInitMu.Lock() 306 defer p.contentInitMu.Unlock() 307 308 err = p.prepareForRender() 309 if err != nil { 310 p.s.Log.ERROR.Printf("Failed to prepare page %q for render: %s", p.Path(), err) 311 return 312 } 313 314 if len(p.summary) == 0 { 315 if err = p.setAutoSummary(); err != nil { 316 err = fmt.Errorf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err) 317 } 318 } 319 c <- err 320 }() 321 322 select { 323 case <-ctx.Done(): 324 p.s.Log.WARN.Printf("WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set \"timeout=20000\" (or higher, value is in milliseconds) in config.toml.\n", p.pathOrTitle()) 325 case err := <-c: 326 if err != nil { 327 p.s.Log.ERROR.Println(err) 328 } 329 } 330 }) 331 332 } 333 334 // This is sent to the shortcodes for this page. Not doing that will create an infinite regress. So, 335 // shortcodes can access .Page.TableOfContents, but not .Page.Content etc. 336 func (p *Page) withoutContent() *PageWithoutContent { 337 p.pageInit.withoutContentInit.Do(func() { 338 p.pageWithoutContent = &PageWithoutContent{Page: p} 339 }) 340 return p.pageWithoutContent 341 } 342 343 func (p *Page) Content() (interface{}, error) { 344 return p.content(), nil 345 } 346 347 func (p *Page) Truncated() bool { 348 p.initContent() 349 return p.truncated 350 } 351 352 func (p *Page) content() template.HTML { 353 p.initContent() 354 return p.contentv 355 } 356 357 func (p *Page) Summary() template.HTML { 358 p.initContent() 359 return p.summary 360 } 361 362 // Sites is a convenience method to get all the Hugo sites/languages configured. 363 func (p *Page) Sites() SiteInfos { 364 infos := make(SiteInfos, len(p.s.owner.Sites)) 365 for i, site := range p.s.owner.Sites { 366 infos[i] = &site.Info 367 } 368 369 return infos 370 } 371 372 // SearchKeywords implements the related.Document interface needed for fast page searches. 373 func (p *Page) SearchKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { 374 375 v, err := p.Param(cfg.Name) 376 if err != nil { 377 return nil, err 378 } 379 380 return cfg.ToKeywords(v) 381 } 382 383 // PubDate is when this page was or will be published. 384 // NOTE: This is currently used for search only and is not meant to be used 385 // directly in templates. We need to consolidate the dates in this struct. 386 // TODO(bep) see https://github.com/gohugoio/hugo/issues/3854 387 func (p *Page) PubDate() time.Time { 388 if !p.PublishDate.IsZero() { 389 return p.PublishDate 390 } 391 return p.Date 392 } 393 394 func (*Page) ResourceType() string { 395 return pageResourceType 396 } 397 398 func (p *Page) RSSLink() template.URL { 399 f, found := p.outputFormats.GetByName(output.RSSFormat.Name) 400 if !found { 401 return "" 402 } 403 return template.URL(newOutputFormat(p, f).Permalink()) 404 } 405 406 func (p *Page) createLayoutDescriptor() output.LayoutDescriptor { 407 var section string 408 409 switch p.Kind { 410 case KindSection: 411 // In Hugo 0.22 we introduce nested sections, but we still only 412 // use the first level to pick the correct template. This may change in 413 // the future. 414 section = p.sections[0] 415 case KindTaxonomy, KindTaxonomyTerm: 416 section = p.s.taxonomiesPluralSingular[p.sections[0]] 417 default: 418 } 419 420 return output.LayoutDescriptor{ 421 Kind: p.Kind, 422 Type: p.Type(), 423 Lang: p.Lang(), 424 Layout: p.Layout, 425 Section: section, 426 } 427 } 428 429 // pageInit lazy initializes different parts of the page. It is extracted 430 // into its own type so we can easily create a copy of a given page. 431 type pageInit struct { 432 languageInit sync.Once 433 pageMenusInit sync.Once 434 pageMetaInit sync.Once 435 renderingConfigInit sync.Once 436 withoutContentInit sync.Once 437 } 438 439 type pageContentInit struct { 440 contentInitMu sync.Mutex 441 contentInit sync.Once 442 plainInit sync.Once 443 plainWordsInit sync.Once 444 } 445 446 func (p *Page) resetContent() { 447 p.pageContentInit = &pageContentInit{} 448 } 449 450 // IsNode returns whether this is an item of one of the list types in Hugo, 451 // i.e. not a regular content page. 452 func (p *Page) IsNode() bool { 453 return p.Kind != KindPage 454 } 455 456 // IsHome returns whether this is the home page. 457 func (p *Page) IsHome() bool { 458 return p.Kind == KindHome 459 } 460 461 // IsSection returns whether this is a section page. 462 func (p *Page) IsSection() bool { 463 return p.Kind == KindSection 464 } 465 466 // IsPage returns whether this is a regular content page. 467 func (p *Page) IsPage() bool { 468 return p.Kind == KindPage 469 } 470 471 // BundleType returns the bundle type: "leaf", "branch" or an empty string if it is none. 472 // See https://gohugo.io/content-management/page-bundles/ 473 func (p *Page) BundleType() string { 474 if p.IsNode() { 475 return "branch" 476 } 477 478 var source interface{} = p.Source.File 479 if fi, ok := source.(*fileInfo); ok { 480 switch fi.bundleTp { 481 case bundleBranch: 482 return "branch" 483 case bundleLeaf: 484 return "leaf" 485 } 486 } 487 488 return "" 489 } 490 491 func (p *Page) MediaType() media.Type { 492 return media.OctetType 493 } 494 495 type Source struct { 496 Frontmatter []byte 497 Content []byte 498 source.File 499 } 500 type PageMeta struct { 501 wordCount int 502 fuzzyWordCount int 503 readingTime int 504 Weight int 505 } 506 507 type Position struct { 508 Prev *Page 509 Next *Page 510 PrevInSection *Page 511 NextInSection *Page 512 } 513 514 type Pages []*Page 515 516 func (ps Pages) String() string { 517 return fmt.Sprintf("Pages(%d)", len(ps)) 518 } 519 520 func (ps Pages) findPagePosByFilename(filename string) int { 521 for i, x := range ps { 522 if x.Source.Filename() == filename { 523 return i 524 } 525 } 526 return -1 527 } 528 529 func (ps Pages) removeFirstIfFound(p *Page) Pages { 530 ii := -1 531 for i, pp := range ps { 532 if pp == p { 533 ii = i 534 break 535 } 536 } 537 538 if ii != -1 { 539 ps = append(ps[:ii], ps[ii+1:]...) 540 } 541 return ps 542 } 543 544 func (ps Pages) findPagePosByFilnamePrefix(prefix string) int { 545 if prefix == "" { 546 return -1 547 } 548 549 lenDiff := -1 550 currPos := -1 551 prefixLen := len(prefix) 552 553 // Find the closest match 554 for i, x := range ps { 555 if strings.HasPrefix(x.Source.Filename(), prefix) { 556 diff := len(x.Source.Filename()) - prefixLen 557 if lenDiff == -1 || diff < lenDiff { 558 lenDiff = diff 559 currPos = i 560 } 561 } 562 } 563 return currPos 564 } 565 566 // findPagePos Given a page, it will find the position in Pages 567 // will return -1 if not found 568 func (ps Pages) findPagePos(page *Page) int { 569 for i, x := range ps { 570 if x.Source.Filename() == page.Source.Filename() { 571 return i 572 } 573 } 574 return -1 575 } 576 577 func (p *Page) createWorkContentCopy() { 578 p.workContent = make([]byte, len(p.rawContent)) 579 copy(p.workContent, p.rawContent) 580 } 581 582 func (p *Page) Plain() string { 583 p.initContent() 584 p.initPlain(true) 585 return p.plain 586 } 587 588 func (p *Page) initPlain(lock bool) { 589 p.plainInit.Do(func() { 590 if lock { 591 p.contentInitMu.Lock() 592 defer p.contentInitMu.Unlock() 593 } 594 p.plain = helpers.StripHTML(string(p.contentv)) 595 }) 596 } 597 598 func (p *Page) PlainWords() []string { 599 p.initContent() 600 p.initPlainWords(true) 601 return p.plainWords 602 } 603 604 func (p *Page) initPlainWords(lock bool) { 605 p.plainWordsInit.Do(func() { 606 if lock { 607 p.contentInitMu.Lock() 608 defer p.contentInitMu.Unlock() 609 } 610 p.plainWords = strings.Fields(p.plain) 611 }) 612 } 613 614 // Param is a convenience method to do lookups in Page's and Site's Params map, 615 // in that order. 616 // 617 // This method is also implemented on Node and SiteInfo. 618 func (p *Page) Param(key interface{}) (interface{}, error) { 619 keyStr, err := cast.ToStringE(key) 620 if err != nil { 621 return nil, err 622 } 623 624 keyStr = strings.ToLower(keyStr) 625 result, _ := p.traverseDirect(keyStr) 626 if result != nil { 627 return result, nil 628 } 629 630 keySegments := strings.Split(keyStr, ".") 631 if len(keySegments) == 1 { 632 return nil, nil 633 } 634 635 return p.traverseNested(keySegments) 636 } 637 638 func (p *Page) traverseDirect(key string) (interface{}, error) { 639 keyStr := strings.ToLower(key) 640 if val, ok := p.params[keyStr]; ok { 641 return val, nil 642 } 643 644 return p.Site.Params[keyStr], nil 645 } 646 647 func (p *Page) traverseNested(keySegments []string) (interface{}, error) { 648 result := traverse(keySegments, p.params) 649 if result != nil { 650 return result, nil 651 } 652 653 result = traverse(keySegments, p.Site.Params) 654 if result != nil { 655 return result, nil 656 } 657 658 // Didn't find anything, but also no problems. 659 return nil, nil 660 } 661 662 func traverse(keys []string, m map[string]interface{}) interface{} { 663 // Shift first element off. 664 firstKey, rest := keys[0], keys[1:] 665 result := m[firstKey] 666 667 // No point in continuing here. 668 if result == nil { 669 return result 670 } 671 672 if len(rest) == 0 { 673 // That was the last key. 674 return result 675 } 676 677 // That was not the last key. 678 return traverse(rest, cast.ToStringMap(result)) 679 } 680 681 func (p *Page) Author() Author { 682 authors := p.Authors() 683 684 for _, author := range authors { 685 return author 686 } 687 return Author{} 688 } 689 690 func (p *Page) Authors() AuthorList { 691 authorKeys, ok := p.params["authors"] 692 if !ok { 693 return AuthorList{} 694 } 695 authors := authorKeys.([]string) 696 if len(authors) < 1 || len(p.Site.Authors) < 1 { 697 return AuthorList{} 698 } 699 700 al := make(AuthorList) 701 for _, author := range authors { 702 a, ok := p.Site.Authors[author] 703 if ok { 704 al[author] = a 705 } 706 } 707 return al 708 } 709 710 func (p *Page) UniqueID() string { 711 return p.Source.UniqueID() 712 } 713 714 // for logging 715 func (p *Page) lineNumRawContentStart() int { 716 return bytes.Count(p.frontmatter, []byte("\n")) + 1 717 } 718 719 var ( 720 internalSummaryDivider = []byte("HUGOMORE42") 721 ) 722 723 // replaceDivider replaces the <!--more--> with an internal value and returns 724 // whether the contentis truncated or not. 725 // Note: The content slice will be modified if needed. 726 func replaceDivider(content, from, to []byte) ([]byte, bool) { 727 dividerIdx := bytes.Index(content, from) 728 if dividerIdx == -1 { 729 return content, false 730 } 731 732 afterSummary := content[dividerIdx+len(from):] 733 734 // If the raw content has nothing but whitespace after the summary 735 // marker then the page shouldn't be marked as truncated. This check 736 // is simplest against the raw content because different markup engines 737 // (rst and asciidoc in particular) add div and p elements after the 738 // summary marker. 739 truncated := bytes.IndexFunc(afterSummary, func(r rune) bool { return !unicode.IsSpace(r) }) != -1 740 741 content = append(content[:dividerIdx], append(to, afterSummary...)...) 742 743 return content, truncated 744 745 } 746 747 // We have to replace the <!--more--> with something that survives all the 748 // rendering engines. 749 func (p *Page) replaceDivider(content []byte) []byte { 750 summaryDivider := helpers.SummaryDivider 751 if p.Markup == "org" { 752 summaryDivider = []byte("# more") 753 } 754 755 replaced, truncated := replaceDivider(content, summaryDivider, internalSummaryDivider) 756 757 p.truncated = truncated 758 759 return replaced 760 } 761 762 // Returns the page as summary and main if a user defined split is provided. 763 func (p *Page) setUserDefinedSummaryIfProvided(rawContentCopy []byte) (*summaryContent, error) { 764 765 sc, err := splitUserDefinedSummaryAndContent(p.Markup, rawContentCopy) 766 767 if err != nil { 768 return nil, err 769 } 770 771 if sc == nil { 772 // No divider found 773 return nil, nil 774 } 775 776 p.summary = helpers.BytesToHTML(sc.summary) 777 778 return sc, nil 779 } 780 781 // Make this explicit so there is no doubt about what is what. 782 type summaryContent struct { 783 summary []byte 784 content []byte 785 } 786 787 func splitUserDefinedSummaryAndContent(markup string, c []byte) (sc *summaryContent, err error) { 788 defer func() { 789 if r := recover(); r != nil { 790 err = fmt.Errorf("summary split failed: %s", r) 791 } 792 }() 793 794 c = bytes.TrimSpace(c) 795 startDivider := bytes.Index(c, internalSummaryDivider) 796 797 if startDivider == -1 { 798 return 799 } 800 801 endDivider := startDivider + len(internalSummaryDivider) 802 endSummary := startDivider 803 804 var ( 805 startMarkup []byte 806 endMarkup []byte 807 addDiv bool 808 ) 809 810 switch markup { 811 default: 812 startMarkup = []byte("<p>") 813 endMarkup = []byte("</p>") 814 case "asciidoc": 815 startMarkup = []byte("<div class=\"paragraph\">") 816 endMarkup = []byte("</div>") 817 case "rst": 818 startMarkup = []byte("<p>") 819 endMarkup = []byte("</p>") 820 addDiv = true 821 } 822 823 // Find the closest end/start markup string to the divider 824 fromStart := -1 825 fromIdx := bytes.LastIndex(c[:startDivider], startMarkup) 826 if fromIdx != -1 { 827 fromStart = startDivider - fromIdx - len(startMarkup) 828 } 829 fromEnd := bytes.Index(c[endDivider:], endMarkup) 830 831 if fromEnd != -1 && fromEnd <= fromStart { 832 endSummary = startDivider + fromEnd + len(endMarkup) 833 } else if fromStart != -1 && fromEnd != -1 { 834 endSummary = startDivider - fromStart - len(startMarkup) 835 } 836 837 withoutDivider := bytes.TrimSpace(append(c[:startDivider], c[endDivider:]...)) 838 var ( 839 summary []byte 840 ) 841 842 if len(withoutDivider) > 0 { 843 summary = bytes.TrimSpace(withoutDivider[:endSummary]) 844 } 845 846 if addDiv { 847 // For the rst 848 summary = append(append([]byte(nil), summary...), []byte("</div>")...) 849 } 850 851 if err != nil { 852 return 853 } 854 855 sc = &summaryContent{ 856 summary: summary, 857 content: withoutDivider, 858 } 859 860 return 861 } 862 863 func (p *Page) setAutoSummary() error { 864 var summary string 865 var truncated bool 866 // This careful init dance could probably be refined, but it is purely for performance 867 // reasons. These "plain" methods are expensive if the plain content is never actually 868 // used. 869 p.initPlain(false) 870 if p.isCJKLanguage { 871 p.initPlainWords(false) 872 summary, truncated = p.s.ContentSpec.TruncateWordsByRune(p.plainWords) 873 } else { 874 summary, truncated = p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain) 875 } 876 p.summary = template.HTML(summary) 877 p.truncated = truncated 878 879 return nil 880 881 } 882 883 func (p *Page) renderContent(content []byte) []byte { 884 return p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{ 885 Content: content, RenderTOC: true, PageFmt: p.Markup, 886 Cfg: p.Language(), 887 DocumentID: p.UniqueID(), DocumentName: p.Path(), 888 Config: p.getRenderingConfig()}) 889 } 890 891 func (p *Page) getRenderingConfig() *helpers.BlackFriday { 892 p.renderingConfigInit.Do(func() { 893 bfParam := p.getParamToLower("blackfriday") 894 if bfParam == nil { 895 p.renderingConfig = p.s.ContentSpec.BlackFriday 896 return 897 } 898 // Create a copy so we can modify it. 899 bf := *p.s.ContentSpec.BlackFriday 900 p.renderingConfig = &bf 901 902 if p.Language() == nil { 903 panic(fmt.Sprintf("nil language for %s with source lang %s", p.BaseFileName(), p.lang)) 904 } 905 906 pageParam := cast.ToStringMap(bfParam) 907 if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil { 908 p.s.Log.FATAL.Printf("Failed to get rendering config for %s:\n%s", p.BaseFileName(), err.Error()) 909 } 910 911 }) 912 913 return p.renderingConfig 914 } 915 916 func (s *Site) newPage(filename string) *Page { 917 fi := newFileInfo( 918 s.SourceSpec, 919 s.absContentDir(), 920 filename, 921 nil, 922 bundleNot, 923 ) 924 return s.newPageFromFile(fi) 925 } 926 927 func (s *Site) newPageFromFile(fi *fileInfo) *Page { 928 return &Page{ 929 pageInit: &pageInit{}, 930 pageContentInit: &pageContentInit{}, 931 Kind: kindFromFileInfo(fi), 932 contentType: "", 933 Source: Source{File: fi}, 934 Keywords: []string{}, Sitemap: Sitemap{Priority: -1}, 935 params: make(map[string]interface{}), 936 translations: make(Pages, 0), 937 sections: sectionsFromFile(fi), 938 Site: &s.Info, 939 s: s, 940 } 941 } 942 943 func (p *Page) IsRenderable() bool { 944 return p.renderable 945 } 946 947 func (p *Page) Type() string { 948 if p.contentType != "" { 949 return p.contentType 950 } 951 952 if x := p.Section(); x != "" { 953 return x 954 } 955 956 return "page" 957 } 958 959 // Section returns the first path element below the content root. Note that 960 // since Hugo 0.22 we support nested sections, but this will always be the first 961 // element of any nested path. 962 func (p *Page) Section() string { 963 if p.Kind == KindSection || p.Kind == KindTaxonomy || p.Kind == KindTaxonomyTerm { 964 return p.sections[0] 965 } 966 return p.Source.Section() 967 } 968 969 func (s *Site) NewPageFrom(buf io.Reader, name string) (*Page, error) { 970 p, err := s.NewPage(name) 971 if err != nil { 972 return p, err 973 } 974 _, err = p.ReadFrom(buf) 975 976 return p, err 977 } 978 979 func (s *Site) NewPage(name string) (*Page, error) { 980 if len(name) == 0 { 981 return nil, errors.New("Zero length page name") 982 } 983 984 // Create new page 985 p := s.newPage(name) 986 p.s = s 987 p.Site = &s.Info 988 989 return p, nil 990 } 991 992 func (p *Page) ReadFrom(buf io.Reader) (int64, error) { 993 // Parse for metadata & body 994 if err := p.parse(buf); err != nil { 995 p.s.Log.ERROR.Printf("%s for %s", err, p.File.Path()) 996 return 0, err 997 } 998 999 return int64(len(p.rawContent)), nil 1000 } 1001 1002 func (p *Page) WordCount() int { 1003 p.initContentPlainAndMeta() 1004 return p.wordCount 1005 } 1006 1007 func (p *Page) ReadingTime() int { 1008 p.initContentPlainAndMeta() 1009 return p.readingTime 1010 } 1011 1012 func (p *Page) FuzzyWordCount() int { 1013 p.initContentPlainAndMeta() 1014 return p.fuzzyWordCount 1015 } 1016 1017 func (p *Page) initContentPlainAndMeta() { 1018 p.initContent() 1019 p.initPlain(true) 1020 p.initPlainWords(true) 1021 p.initMeta() 1022 } 1023 1024 func (p *Page) initContentAndMeta() { 1025 p.initContent() 1026 p.initMeta() 1027 } 1028 1029 func (p *Page) initMeta() { 1030 p.pageMetaInit.Do(func() { 1031 if p.isCJKLanguage { 1032 p.wordCount = 0 1033 for _, word := range p.plainWords { 1034 runeCount := utf8.RuneCountInString(word) 1035 if len(word) == runeCount { 1036 p.wordCount++ 1037 } else { 1038 p.wordCount += runeCount 1039 } 1040 } 1041 } else { 1042 p.wordCount = helpers.TotalWords(p.plain) 1043 } 1044 1045 // TODO(bep) is set in a test. Fix that. 1046 if p.fuzzyWordCount == 0 { 1047 p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100 1048 } 1049 1050 if p.isCJKLanguage { 1051 p.readingTime = (p.wordCount + 500) / 501 1052 } else { 1053 p.readingTime = (p.wordCount + 212) / 213 1054 } 1055 }) 1056 } 1057 1058 // HasShortcode return whether the page has a shortcode with the given name. 1059 // This method is mainly motivated with the Hugo Docs site's need for a list 1060 // of pages with the `todo` shortcode in it. 1061 func (p *Page) HasShortcode(name string) bool { 1062 if p.shortcodeState == nil { 1063 return false 1064 } 1065 1066 return p.shortcodeState.nameSet[name] 1067 } 1068 1069 // AllTranslations returns all translations, including the current Page. 1070 func (p *Page) AllTranslations() Pages { 1071 return p.translations 1072 } 1073 1074 // IsTranslated returns whether this content file is translated to 1075 // other language(s). 1076 func (p *Page) IsTranslated() bool { 1077 return len(p.translations) > 1 1078 } 1079 1080 // Translations returns the translations excluding the current Page. 1081 func (p *Page) Translations() Pages { 1082 translations := make(Pages, 0) 1083 for _, t := range p.translations { 1084 if t.Lang() != p.Lang() { 1085 translations = append(translations, t) 1086 } 1087 } 1088 return translations 1089 } 1090 1091 // TranslationKey returns the key used to map language translations of this page. 1092 // It will use the translationKey set in front matter if set, or the content path and 1093 // filename (excluding any language code and extension), e.g. "about/index". 1094 // The Page Kind is always prepended. 1095 func (p *Page) TranslationKey() string { 1096 if p.translationKey != "" { 1097 return p.Kind + "/" + p.translationKey 1098 } 1099 1100 if p.IsNode() { 1101 return path.Join(p.Kind, path.Join(p.sections...), p.TranslationBaseName()) 1102 } 1103 1104 return path.Join(p.Kind, filepath.ToSlash(p.Dir()), p.TranslationBaseName()) 1105 } 1106 1107 func (p *Page) LinkTitle() string { 1108 if len(p.linkTitle) > 0 { 1109 return p.linkTitle 1110 } 1111 return p.title 1112 } 1113 1114 func (p *Page) shouldBuild() bool { 1115 return shouldBuild(p.s.BuildFuture, p.s.BuildExpired, 1116 p.s.BuildDrafts, p.Draft, p.PublishDate, p.ExpiryDate) 1117 } 1118 1119 func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool, 1120 publishDate time.Time, expiryDate time.Time) bool { 1121 if !(buildDrafts || !Draft) { 1122 return false 1123 } 1124 if !buildFuture && !publishDate.IsZero() && publishDate.After(time.Now()) { 1125 return false 1126 } 1127 if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(time.Now()) { 1128 return false 1129 } 1130 return true 1131 } 1132 1133 func (p *Page) IsDraft() bool { 1134 return p.Draft 1135 } 1136 1137 func (p *Page) IsFuture() bool { 1138 if p.PublishDate.IsZero() { 1139 return false 1140 } 1141 return p.PublishDate.After(time.Now()) 1142 } 1143 1144 func (p *Page) IsExpired() bool { 1145 if p.ExpiryDate.IsZero() { 1146 return false 1147 } 1148 return p.ExpiryDate.Before(time.Now()) 1149 } 1150 1151 func (p *Page) URL() string { 1152 1153 if p.IsPage() && p.URLPath.URL != "" { 1154 // This is the url set in front matter 1155 return p.URLPath.URL 1156 } 1157 // Fall back to the relative permalink. 1158 u := p.RelPermalink() 1159 return u 1160 } 1161 1162 // Permalink returns the absolute URL to this Page. 1163 func (p *Page) Permalink() string { 1164 if p.headless { 1165 return "" 1166 } 1167 return p.permalink 1168 } 1169 1170 // RelPermalink gets a URL to the resource relative to the host. 1171 func (p *Page) RelPermalink() string { 1172 if p.headless { 1173 return "" 1174 } 1175 return p.relPermalink 1176 } 1177 1178 // See resource.Resource 1179 // This value is used, by default, in Resources.ByPrefix etc. 1180 func (p *Page) Name() string { 1181 if p.resourcePath != "" { 1182 return p.resourcePath 1183 } 1184 return p.title 1185 } 1186 1187 func (p *Page) Title() string { 1188 return p.title 1189 } 1190 1191 func (p *Page) Params() map[string]interface{} { 1192 return p.params 1193 } 1194 1195 func (p *Page) subResourceTargetPathFactory(base string) string { 1196 return path.Join(p.relTargetPathBase, base) 1197 } 1198 1199 func (p *Page) initMainOutputFormat() error { 1200 if p.mainPageOutput != nil { 1201 return nil 1202 } 1203 1204 outFormat := p.outputFormats[0] 1205 pageOutput, err := newPageOutput(p, false, false, outFormat) 1206 1207 if err != nil { 1208 return fmt.Errorf("Failed to create output page for type %q for page %q: %s", outFormat.Name, p.pathOrTitle(), err) 1209 } 1210 1211 p.mainPageOutput = pageOutput 1212 1213 return nil 1214 1215 } 1216 1217 func (p *Page) setContentInit(start bool) error { 1218 1219 if start { 1220 // This is a new language. 1221 p.shortcodeState.clearDelta() 1222 } 1223 updated := true 1224 if p.shortcodeState != nil { 1225 updated = p.shortcodeState.updateDelta() 1226 } 1227 1228 if updated { 1229 p.resetContent() 1230 } 1231 1232 for _, r := range p.Resources.ByType(pageResourceType) { 1233 p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) 1234 bp := r.(*Page) 1235 if start { 1236 bp.shortcodeState.clearDelta() 1237 } 1238 if bp.shortcodeState != nil { 1239 updated = bp.shortcodeState.updateDelta() 1240 } 1241 if updated { 1242 bp.resetContent() 1243 } 1244 } 1245 1246 return nil 1247 1248 } 1249 1250 func (p *Page) prepareForRender() error { 1251 s := p.s 1252 1253 // If we got this far it means that this is either a new Page pointer 1254 // or a template or similar has changed so wee need to do a rerendering 1255 // of the shortcodes etc. 1256 1257 // If in watch mode or if we have multiple sites or output formats, 1258 // we need to keep the original so we can 1259 // potentially repeat this process on rebuild. 1260 needsACopy := s.running() || len(s.owner.Sites) > 1 || len(p.outputFormats) > 1 1261 var workContentCopy []byte 1262 if needsACopy { 1263 workContentCopy = make([]byte, len(p.workContent)) 1264 copy(workContentCopy, p.workContent) 1265 } else { 1266 // Just reuse the same slice. 1267 workContentCopy = p.workContent 1268 } 1269 1270 var err error 1271 // Note: The shortcodes in a page cannot access the page content it lives in, 1272 // hence the withoutContent(). 1273 if workContentCopy, err = handleShortcodes(p.withoutContent(), workContentCopy); err != nil { 1274 s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) 1275 } 1276 1277 if p.Markup != "html" { 1278 1279 // Now we know enough to create a summary of the page and count some words 1280 summaryContent, err := p.setUserDefinedSummaryIfProvided(workContentCopy) 1281 1282 if err != nil { 1283 s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", p.Path(), err) 1284 } else if summaryContent != nil { 1285 workContentCopy = summaryContent.content 1286 } 1287 1288 p.contentv = helpers.BytesToHTML(workContentCopy) 1289 1290 } else { 1291 p.contentv = helpers.BytesToHTML(workContentCopy) 1292 } 1293 1294 return nil 1295 } 1296 1297 var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter") 1298 1299 func (p *Page) update(frontmatter map[string]interface{}) error { 1300 if frontmatter == nil { 1301 return errors.New("missing frontmatter data") 1302 } 1303 // Needed for case insensitive fetching of params values 1304 maps.ToLower(frontmatter) 1305 1306 var mtime time.Time 1307 if p.Source.FileInfo() != nil { 1308 mtime = p.Source.FileInfo().ModTime() 1309 } 1310 1311 var gitAuthorDate time.Time 1312 if p.GitInfo != nil { 1313 gitAuthorDate = p.GitInfo.AuthorDate 1314 } 1315 1316 descriptor := &pagemeta.FrontMatterDescriptor{ 1317 Frontmatter: frontmatter, 1318 Params: p.params, 1319 Dates: &p.PageDates, 1320 PageURLs: &p.URLPath, 1321 BaseFilename: p.BaseFileName(), 1322 ModTime: mtime, 1323 GitAuthorDate: gitAuthorDate, 1324 } 1325 1326 // Handle the date separately 1327 // TODO(bep) we need to "do more" in this area so this can be split up and 1328 // more easily tested without the Page, but the coupling is strong. 1329 err := p.s.frontmatterHandler.HandleDates(descriptor) 1330 if err != nil { 1331 p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.Path(), err) 1332 } 1333 1334 var draft, published, isCJKLanguage *bool 1335 for k, v := range frontmatter { 1336 loki := strings.ToLower(k) 1337 1338 if loki == "published" { // Intentionally undocumented 1339 vv, err := cast.ToBoolE(v) 1340 if err == nil { 1341 published = &vv 1342 } 1343 // published may also be a date 1344 continue 1345 } 1346 1347 if p.s.frontmatterHandler.IsDateKey(loki) { 1348 continue 1349 } 1350 1351 switch loki { 1352 case "title": 1353 p.title = cast.ToString(v) 1354 p.params[loki] = p.title 1355 case "linktitle": 1356 p.linkTitle = cast.ToString(v) 1357 p.params[loki] = p.linkTitle 1358 case "description": 1359 p.Description = cast.ToString(v) 1360 p.params[loki] = p.Description 1361 case "slug": 1362 p.Slug = cast.ToString(v) 1363 p.params[loki] = p.Slug 1364 case "url": 1365 if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { 1366 return fmt.Errorf("Only relative URLs are supported, %v provided", url) 1367 } 1368 p.URLPath.URL = cast.ToString(v) 1369 p.frontMatterURL = p.URLPath.URL 1370 p.params[loki] = p.URLPath.URL 1371 case "type": 1372 p.contentType = cast.ToString(v) 1373 p.params[loki] = p.contentType 1374 case "extension", "ext": 1375 p.extension = cast.ToString(v) 1376 p.params[loki] = p.extension 1377 case "keywords": 1378 p.Keywords = cast.ToStringSlice(v) 1379 p.params[loki] = p.Keywords 1380 case "headless": 1381 // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output). 1382 // We may expand on this in the future, but that gets more complex pretty fast. 1383 if p.TranslationBaseName() == "index" { 1384 p.headless = cast.ToBool(v) 1385 } 1386 p.params[loki] = p.headless 1387 case "outputs": 1388 o := cast.ToStringSlice(v) 1389 if len(o) > 0 { 1390 // Output formats are exlicitly set in front matter, use those. 1391 outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) 1392 1393 if err != nil { 1394 p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err) 1395 } else { 1396 p.outputFormats = outFormats 1397 p.params[loki] = outFormats 1398 } 1399 1400 } 1401 case "draft": 1402 draft = new(bool) 1403 *draft = cast.ToBool(v) 1404 case "layout": 1405 p.Layout = cast.ToString(v) 1406 p.params[loki] = p.Layout 1407 case "markup": 1408 p.Markup = cast.ToString(v) 1409 p.params[loki] = p.Markup 1410 case "weight": 1411 p.Weight = cast.ToInt(v) 1412 p.params[loki] = p.Weight 1413 case "aliases": 1414 p.Aliases = cast.ToStringSlice(v) 1415 for _, alias := range p.Aliases { 1416 if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { 1417 return fmt.Errorf("Only relative aliases are supported, %v provided", alias) 1418 } 1419 } 1420 p.params[loki] = p.Aliases 1421 case "status": 1422 p.Status = cast.ToString(v) 1423 p.params[loki] = p.Status 1424 case "sitemap": 1425 p.Sitemap = parseSitemap(cast.ToStringMap(v)) 1426 p.params[loki] = p.Sitemap 1427 case "iscjklanguage": 1428 isCJKLanguage = new(bool) 1429 *isCJKLanguage = cast.ToBool(v) 1430 case "translationkey": 1431 p.translationKey = cast.ToString(v) 1432 p.params[loki] = p.translationKey 1433 case "resources": 1434 var resources []map[string]interface{} 1435 handled := true 1436 1437 switch vv := v.(type) { 1438 case []map[interface{}]interface{}: 1439 for _, vvv := range vv { 1440 resources = append(resources, cast.ToStringMap(vvv)) 1441 } 1442 case []map[string]interface{}: 1443 for _, vvv := range vv { 1444 resources = append(resources, vvv) 1445 } 1446 case []interface{}: 1447 for _, vvv := range vv { 1448 switch vvvv := vvv.(type) { 1449 case map[interface{}]interface{}: 1450 resources = append(resources, cast.ToStringMap(vvvv)) 1451 case map[string]interface{}: 1452 resources = append(resources, vvvv) 1453 } 1454 } 1455 default: 1456 handled = false 1457 } 1458 1459 if handled { 1460 p.params[loki] = resources 1461 p.resourcesMetadata = resources 1462 break 1463 } 1464 fallthrough 1465 1466 default: 1467 // If not one of the explicit values, store in Params 1468 switch vv := v.(type) { 1469 case bool: 1470 p.params[loki] = vv 1471 case string: 1472 p.params[loki] = vv 1473 case int64, int32, int16, int8, int: 1474 p.params[loki] = vv 1475 case float64, float32: 1476 p.params[loki] = vv 1477 case time.Time: 1478 p.params[loki] = vv 1479 default: // handle array of strings as well 1480 switch vvv := vv.(type) { 1481 case []interface{}: 1482 if len(vvv) > 0 { 1483 switch vvv[0].(type) { 1484 case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter 1485 p.params[loki] = vvv 1486 case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter 1487 p.params[loki] = vvv 1488 case []interface{}: 1489 p.params[loki] = vvv 1490 default: 1491 a := make([]string, len(vvv)) 1492 for i, u := range vvv { 1493 a[i] = cast.ToString(u) 1494 } 1495 1496 p.params[loki] = a 1497 } 1498 } else { 1499 p.params[loki] = []string{} 1500 } 1501 default: 1502 p.params[loki] = vv 1503 } 1504 } 1505 } 1506 } 1507 1508 // Try markup explicitly set in the frontmatter 1509 p.Markup = helpers.GuessType(p.Markup) 1510 if p.Markup == "unknown" { 1511 // Fall back to file extension (might also return "unknown") 1512 p.Markup = helpers.GuessType(p.Source.Ext()) 1513 } 1514 1515 if draft != nil && published != nil { 1516 p.Draft = *draft 1517 p.s.Log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) 1518 return ErrHasDraftAndPublished 1519 } else if draft != nil { 1520 p.Draft = *draft 1521 } else if published != nil { 1522 p.Draft = !*published 1523 } 1524 p.params["draft"] = p.Draft 1525 1526 if isCJKLanguage != nil { 1527 p.isCJKLanguage = *isCJKLanguage 1528 } else if p.s.Cfg.GetBool("hasCJKLanguage") { 1529 if cjk.Match(p.rawContent) { 1530 p.isCJKLanguage = true 1531 } else { 1532 p.isCJKLanguage = false 1533 } 1534 } 1535 p.params["iscjklanguage"] = p.isCJKLanguage 1536 1537 return nil 1538 } 1539 1540 func (p *Page) GetParam(key string) interface{} { 1541 return p.getParam(key, false) 1542 } 1543 1544 func (p *Page) getParamToLower(key string) interface{} { 1545 return p.getParam(key, true) 1546 } 1547 1548 func (p *Page) getParam(key string, stringToLower bool) interface{} { 1549 v := p.params[strings.ToLower(key)] 1550 1551 if v == nil { 1552 return nil 1553 } 1554 1555 switch val := v.(type) { 1556 case bool: 1557 return val 1558 case string: 1559 if stringToLower { 1560 return strings.ToLower(val) 1561 } 1562 return val 1563 case int64, int32, int16, int8, int: 1564 return cast.ToInt(v) 1565 case float64, float32: 1566 return cast.ToFloat64(v) 1567 case time.Time: 1568 return val 1569 case []string: 1570 if stringToLower { 1571 return helpers.SliceToLower(val) 1572 } 1573 return v 1574 case map[string]interface{}: // JSON and TOML 1575 return v 1576 case map[interface{}]interface{}: // YAML 1577 return v 1578 } 1579 1580 p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) 1581 return nil 1582 } 1583 1584 func (p *Page) HasMenuCurrent(menuID string, me *MenuEntry) bool { 1585 1586 sectionPagesMenu := p.Site.sectionPagesMenu 1587 1588 // page is labeled as "shadow-member" of the menu with the same identifier as the section 1589 if sectionPagesMenu != "" { 1590 section := p.Section() 1591 1592 if section != "" && sectionPagesMenu == menuID && section == me.Identifier { 1593 return true 1594 } 1595 } 1596 1597 if !me.HasChildren() { 1598 return false 1599 } 1600 1601 menus := p.Menus() 1602 1603 if m, ok := menus[menuID]; ok { 1604 1605 for _, child := range me.Children { 1606 if child.IsEqual(m) { 1607 return true 1608 } 1609 if p.HasMenuCurrent(menuID, child) { 1610 return true 1611 } 1612 } 1613 1614 } 1615 1616 if p.IsPage() { 1617 return false 1618 } 1619 1620 // The following logic is kept from back when Hugo had both Page and Node types. 1621 // TODO(bep) consolidate / clean 1622 nme := MenuEntry{Page: p, Name: p.title, URL: p.URL()} 1623 1624 for _, child := range me.Children { 1625 if nme.IsSameResource(child) { 1626 return true 1627 } 1628 if p.HasMenuCurrent(menuID, child) { 1629 return true 1630 } 1631 } 1632 1633 return false 1634 1635 } 1636 1637 func (p *Page) IsMenuCurrent(menuID string, inme *MenuEntry) bool { 1638 1639 menus := p.Menus() 1640 1641 if me, ok := menus[menuID]; ok { 1642 if me.IsEqual(inme) { 1643 return true 1644 } 1645 } 1646 1647 if p.IsPage() { 1648 return false 1649 } 1650 1651 // The following logic is kept from back when Hugo had both Page and Node types. 1652 // TODO(bep) consolidate / clean 1653 me := MenuEntry{Page: p, Name: p.title, URL: p.URL()} 1654 1655 if !me.IsSameResource(inme) { 1656 return false 1657 } 1658 1659 // this resource may be included in several menus 1660 // search for it to make sure that it is in the menu with the given menuId 1661 if menu, ok := (*p.Site.Menus)[menuID]; ok { 1662 for _, menuEntry := range *menu { 1663 if menuEntry.IsSameResource(inme) { 1664 return true 1665 } 1666 1667 descendantFound := p.isSameAsDescendantMenu(inme, menuEntry) 1668 if descendantFound { 1669 return descendantFound 1670 } 1671 1672 } 1673 } 1674 1675 return false 1676 } 1677 1678 func (p *Page) isSameAsDescendantMenu(inme *MenuEntry, parent *MenuEntry) bool { 1679 if parent.HasChildren() { 1680 for _, child := range parent.Children { 1681 if child.IsSameResource(inme) { 1682 return true 1683 } 1684 descendantFound := p.isSameAsDescendantMenu(inme, child) 1685 if descendantFound { 1686 return descendantFound 1687 } 1688 } 1689 } 1690 return false 1691 } 1692 1693 func (p *Page) Menus() PageMenus { 1694 p.pageMenusInit.Do(func() { 1695 p.pageMenus = PageMenus{} 1696 1697 if ms, ok := p.params["menu"]; ok { 1698 link := p.RelPermalink() 1699 1700 me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight, URL: link} 1701 1702 // Could be the name of the menu to attach it to 1703 mname, err := cast.ToStringE(ms) 1704 1705 if err == nil { 1706 me.Menu = mname 1707 p.pageMenus[mname] = &me 1708 return 1709 } 1710 1711 // Could be a slice of strings 1712 mnames, err := cast.ToStringSliceE(ms) 1713 1714 if err == nil { 1715 for _, mname := range mnames { 1716 me.Menu = mname 1717 p.pageMenus[mname] = &me 1718 } 1719 return 1720 } 1721 1722 // Could be a structured menu entry 1723 menus, err := cast.ToStringMapE(ms) 1724 1725 if err != nil { 1726 p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.title) 1727 } 1728 1729 for name, menu := range menus { 1730 menuEntry := MenuEntry{Page: p, Name: p.LinkTitle(), URL: link, Weight: p.Weight, Menu: name} 1731 if menu != nil { 1732 p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.title) 1733 ime, err := cast.ToStringMapE(menu) 1734 if err != nil { 1735 p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.title, err) 1736 } 1737 1738 menuEntry.marshallMap(ime) 1739 } 1740 p.pageMenus[name] = &menuEntry 1741 1742 } 1743 } 1744 }) 1745 1746 return p.pageMenus 1747 } 1748 1749 func (p *Page) shouldRenderTo(f output.Format) bool { 1750 _, found := p.outputFormats.GetByName(f.Name) 1751 return found 1752 } 1753 1754 func (p *Page) parse(reader io.Reader) error { 1755 psr, err := parser.ReadFrom(reader) 1756 if err != nil { 1757 return err 1758 } 1759 1760 p.renderable = psr.IsRenderable() 1761 p.frontmatter = psr.FrontMatter() 1762 p.rawContent = psr.Content() 1763 p.lang = p.Source.File.Lang() 1764 1765 meta, err := psr.Metadata() 1766 if err != nil { 1767 return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) 1768 } 1769 if meta == nil { 1770 // missing frontmatter equivalent to empty frontmatter 1771 meta = map[string]interface{}{} 1772 } 1773 1774 if p.s != nil && p.s.owner != nil { 1775 gi, enabled := p.s.owner.gitInfo.forPage(p) 1776 if gi != nil { 1777 p.GitInfo = gi 1778 } else if enabled { 1779 p.s.Log.WARN.Printf("Failed to find GitInfo for page %q", p.Path()) 1780 } 1781 } 1782 1783 return p.update(meta) 1784 } 1785 1786 func (p *Page) RawContent() string { 1787 return string(p.rawContent) 1788 } 1789 1790 func (p *Page) SetSourceContent(content []byte) { 1791 p.Source.Content = content 1792 } 1793 1794 func (p *Page) SetSourceMetaData(in interface{}, mark rune) (err error) { 1795 // See https://github.com/gohugoio/hugo/issues/2458 1796 defer func() { 1797 if r := recover(); r != nil { 1798 var ok bool 1799 err, ok = r.(error) 1800 if !ok { 1801 err = fmt.Errorf("error from marshal: %v", r) 1802 } 1803 } 1804 }() 1805 1806 buf := bp.GetBuffer() 1807 defer bp.PutBuffer(buf) 1808 1809 err = parser.InterfaceToFrontMatter(in, mark, buf) 1810 if err != nil { 1811 return 1812 } 1813 1814 _, err = buf.WriteRune('\n') 1815 if err != nil { 1816 return 1817 } 1818 1819 p.Source.Frontmatter = buf.Bytes() 1820 1821 return 1822 } 1823 1824 func (p *Page) SafeSaveSourceAs(path string) error { 1825 return p.saveSourceAs(path, true) 1826 } 1827 1828 func (p *Page) SaveSourceAs(path string) error { 1829 return p.saveSourceAs(path, false) 1830 } 1831 1832 func (p *Page) saveSourceAs(path string, safe bool) error { 1833 b := bp.GetBuffer() 1834 defer bp.PutBuffer(b) 1835 1836 b.Write(p.Source.Frontmatter) 1837 b.Write(p.Source.Content) 1838 1839 bc := make([]byte, b.Len(), b.Len()) 1840 copy(bc, b.Bytes()) 1841 1842 return p.saveSource(bc, path, safe) 1843 } 1844 1845 func (p *Page) saveSource(by []byte, inpath string, safe bool) (err error) { 1846 if !filepath.IsAbs(inpath) { 1847 inpath = p.s.PathSpec.AbsPathify(inpath) 1848 } 1849 p.s.Log.INFO.Println("creating", inpath) 1850 if safe { 1851 err = helpers.SafeWriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) 1852 } else { 1853 err = helpers.WriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) 1854 } 1855 if err != nil { 1856 return 1857 } 1858 return nil 1859 } 1860 1861 func (p *Page) SaveSource() error { 1862 return p.SaveSourceAs(p.FullFilePath()) 1863 } 1864 1865 // TODO(bep) lazy consolidate 1866 func (p *Page) processShortcodes() error { 1867 p.shortcodeState = newShortcodeHandler(p) 1868 tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p.withoutContent()) 1869 if err != nil { 1870 return err 1871 } 1872 p.workContent = []byte(tmpContent) 1873 1874 return nil 1875 1876 } 1877 1878 func (p *Page) FullFilePath() string { 1879 return filepath.Join(p.Dir(), p.LogicalName()) 1880 } 1881 1882 // Returns the canonical, absolute fully-qualifed logical reference used by 1883 // methods such as GetPage and ref/relref shortcodes to refer to 1884 // this page. It is prefixed with a "/". 1885 // 1886 // For pages that have a source file, it is returns the path to this file as an 1887 // absolute path rooted in this site's content dir. 1888 // For pages that do not (sections witout content page etc.), it returns the 1889 // virtual path, consistent with where you would add a source file. 1890 func (p *Page) absoluteSourceRef() string { 1891 if p.Source.File != nil { 1892 sourcePath := p.Source.Path() 1893 if sourcePath != "" { 1894 return "/" + filepath.ToSlash(sourcePath) 1895 } 1896 } 1897 1898 if len(p.sections) > 0 { 1899 // no backing file, return the virtual source path 1900 return "/" + path.Join(p.sections...) 1901 } 1902 1903 return "" 1904 } 1905 1906 // Pre render prepare steps 1907 1908 func (p *Page) prepareLayouts() error { 1909 // TODO(bep): Check the IsRenderable logic. 1910 if p.Kind == KindPage { 1911 if !p.IsRenderable() { 1912 self := "__" + p.UniqueID() 1913 err := p.s.TemplateHandler().AddLateTemplate(self, string(p.content())) 1914 if err != nil { 1915 return err 1916 } 1917 p.selfLayout = self 1918 } 1919 } 1920 1921 return nil 1922 } 1923 1924 func (p *Page) prepareData(s *Site) error { 1925 if p.Kind != KindSection { 1926 var pages Pages 1927 p.data = make(map[string]interface{}) 1928 1929 switch p.Kind { 1930 case KindPage: 1931 case KindHome: 1932 pages = s.RegularPages 1933 case KindTaxonomy: 1934 plural := p.sections[0] 1935 term := p.sections[1] 1936 1937 if s.Info.preserveTaxonomyNames { 1938 if v, ok := s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, term)]; ok { 1939 term = v 1940 } 1941 } 1942 1943 singular := s.taxonomiesPluralSingular[plural] 1944 taxonomy := s.Taxonomies[plural].Get(term) 1945 1946 p.data[singular] = taxonomy 1947 p.data["Singular"] = singular 1948 p.data["Plural"] = plural 1949 p.data["Term"] = term 1950 pages = taxonomy.Pages() 1951 case KindTaxonomyTerm: 1952 plural := p.sections[0] 1953 singular := s.taxonomiesPluralSingular[plural] 1954 1955 p.data["Singular"] = singular 1956 p.data["Plural"] = plural 1957 p.data["Terms"] = s.Taxonomies[plural] 1958 // keep the following just for legacy reasons 1959 p.data["OrderedIndex"] = p.data["Terms"] 1960 p.data["Index"] = p.data["Terms"] 1961 1962 // A list of all KindTaxonomy pages with matching plural 1963 for _, p := range s.findPagesByKind(KindTaxonomy) { 1964 if p.sections[0] == plural { 1965 pages = append(pages, p) 1966 } 1967 } 1968 } 1969 1970 p.data["Pages"] = pages 1971 p.Pages = pages 1972 } 1973 1974 // Now we know enough to set missing dates on home page etc. 1975 p.updatePageDates() 1976 1977 return nil 1978 } 1979 1980 func (p *Page) updatePageDates() { 1981 // TODO(bep) there is a potential issue with page sorting for home pages 1982 // etc. without front matter dates set, but let us wrap the head around 1983 // that in another time. 1984 if !p.IsNode() { 1985 return 1986 } 1987 1988 if !p.Date.IsZero() { 1989 if p.Lastmod.IsZero() { 1990 p.Lastmod = p.Date 1991 } 1992 return 1993 } else if !p.Lastmod.IsZero() { 1994 if p.Date.IsZero() { 1995 p.Date = p.Lastmod 1996 } 1997 return 1998 } 1999 2000 // Set it to the first non Zero date in children 2001 var foundDate, foundLastMod bool 2002 2003 for _, child := range p.Pages { 2004 if !child.Date.IsZero() { 2005 p.Date = child.Date 2006 foundDate = true 2007 } 2008 if !child.Lastmod.IsZero() { 2009 p.Lastmod = child.Lastmod 2010 foundLastMod = true 2011 } 2012 2013 if foundDate && foundLastMod { 2014 break 2015 } 2016 } 2017 } 2018 2019 // copy creates a copy of this page with the lazy sync.Once vars reset 2020 // so they will be evaluated again, for word count calculations etc. 2021 func (p *Page) copy(initContent bool) *Page { 2022 p.contentInitMu.Lock() 2023 c := *p 2024 p.contentInitMu.Unlock() 2025 c.pageInit = &pageInit{} 2026 if initContent { 2027 if len(p.outputFormats) < 2 { 2028 panic(fmt.Sprintf("programming error: page %q should not need to rebuild content as it has only %d outputs", p.Path(), len(p.outputFormats))) 2029 } 2030 c.pageContentInit = &pageContentInit{} 2031 } 2032 return &c 2033 } 2034 2035 func (p *Page) Hugo() *HugoInfo { 2036 return hugoInfo 2037 } 2038 2039 // GetPage looks up a page for the given ref. 2040 // {{ with .GetPage "blog" }}{{ .Title }}{{ end }} 2041 // 2042 // This will return nil when no page could be found, and will return 2043 // an error if the ref is ambiguous. 2044 func (p *Page) GetPage(ref string) (*Page, error) { 2045 return p.s.getPageNew(p, ref) 2046 } 2047 2048 type refArgs struct { 2049 Path string 2050 Lang string 2051 OutputFormat string 2052 } 2053 2054 func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *SiteInfo, error) { 2055 var ra refArgs 2056 err := mapstructure.WeakDecode(args, &ra) 2057 if err != nil { 2058 return ra, nil, nil 2059 } 2060 s := p.Site 2061 2062 if ra.Lang != "" && ra.Lang != p.Lang() { 2063 // Find correct site 2064 found := false 2065 for _, ss := range p.s.owner.Sites { 2066 if ss.Lang() == ra.Lang { 2067 found = true 2068 s = &ss.Info 2069 } 2070 } 2071 2072 if !found { 2073 p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), p) 2074 return ra, nil, nil 2075 } 2076 } 2077 2078 return ra, s, nil 2079 } 2080 2081 func (p *Page) Ref(argsm map[string]interface{}) (string, error) { 2082 args, s, err := p.decodeRefArgs(argsm) 2083 if err != nil { 2084 return "", fmt.Errorf("invalid arguments to Ref: %s", err) 2085 } 2086 2087 if s == nil { 2088 return p.s.siteRefLinker.notFoundURL, nil 2089 } 2090 2091 if args.Path == "" { 2092 return "", nil 2093 } 2094 2095 if args.OutputFormat != "" { 2096 return s.Ref(args.Path, p, args.OutputFormat) 2097 } 2098 return s.Ref(args.Path, p) 2099 } 2100 2101 func (p *Page) RelRef(argsm map[string]interface{}) (string, error) { 2102 args, s, err := p.decodeRefArgs(argsm) 2103 if err != nil { 2104 return "", fmt.Errorf("invalid arguments to Ref: %s", err) 2105 } 2106 2107 if s == nil { 2108 return p.s.siteRefLinker.notFoundURL, nil 2109 } 2110 2111 if args.Path == "" { 2112 return "", nil 2113 } 2114 2115 if args.OutputFormat != "" { 2116 return s.RelRef(args.Path, p, args.OutputFormat) 2117 } 2118 return s.RelRef(args.Path, p) 2119 } 2120 2121 func (p *Page) String() string { 2122 if sourceRef := p.absoluteSourceRef(); sourceRef != "" { 2123 return fmt.Sprintf("Page(%s)", sourceRef) 2124 } 2125 return fmt.Sprintf("Page(%q)", p.title) 2126 } 2127 2128 // Scratch returns the writable context associated with this Page. 2129 func (p *Page) Scratch() *maps.Scratch { 2130 if p.scratch == nil { 2131 p.scratch = maps.NewScratch() 2132 } 2133 return p.scratch 2134 } 2135 2136 func (p *Page) Language() *langs.Language { 2137 p.initLanguage() 2138 return p.language 2139 } 2140 2141 func (p *Page) Lang() string { 2142 // When set, Language can be different from lang in the case where there is a 2143 // content file (doc.sv.md) with language indicator, but there is no language 2144 // config for that language. Then the language will fall back on the site default. 2145 if p.Language() != nil { 2146 return p.Language().Lang 2147 } 2148 return p.lang 2149 } 2150 2151 func (p *Page) isNewTranslation(candidate *Page) bool { 2152 2153 if p.Kind != candidate.Kind { 2154 return false 2155 } 2156 2157 if p.Kind == KindPage || p.Kind == kindUnknown { 2158 panic("Node type not currently supported for this op") 2159 } 2160 2161 // At this point, we know that this is a traditional Node (home page, section, taxonomy) 2162 // It represents the same node, but different language, if the sections is the same. 2163 if len(p.sections) != len(candidate.sections) { 2164 return false 2165 } 2166 2167 for i := 0; i < len(p.sections); i++ { 2168 if p.sections[i] != candidate.sections[i] { 2169 return false 2170 } 2171 } 2172 2173 // Finally check that it is not already added. 2174 for _, translation := range p.translations { 2175 if candidate == translation { 2176 return false 2177 } 2178 } 2179 2180 return true 2181 2182 } 2183 2184 func (p *Page) shouldAddLanguagePrefix() bool { 2185 if !p.Site.IsMultiLingual() { 2186 return false 2187 } 2188 2189 if p.s.owner.IsMultihost() { 2190 return true 2191 } 2192 2193 if p.Lang() == "" { 2194 return false 2195 } 2196 2197 if !p.Site.defaultContentLanguageInSubdir && p.Lang() == p.s.multilingual().DefaultLang.Lang { 2198 return false 2199 } 2200 2201 return true 2202 } 2203 2204 func (p *Page) initLanguage() { 2205 p.languageInit.Do(func() { 2206 if p.language != nil { 2207 return 2208 } 2209 2210 ml := p.s.multilingual() 2211 if ml == nil { 2212 panic("Multilanguage not set") 2213 } 2214 if p.lang == "" { 2215 p.lang = ml.DefaultLang.Lang 2216 p.language = ml.DefaultLang 2217 return 2218 } 2219 2220 language := ml.Language(p.lang) 2221 2222 if language == nil { 2223 // It can be a file named stefano.chiodino.md. 2224 p.s.Log.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", p.lang) 2225 language = ml.DefaultLang 2226 } 2227 2228 p.language = language 2229 2230 }) 2231 } 2232 2233 func (p *Page) LanguagePrefix() string { 2234 return p.Site.LanguagePrefix 2235 } 2236 2237 func (p *Page) addLangPathPrefixIfFlagSet(outfile string, should bool) string { 2238 if helpers.IsAbsURL(outfile) { 2239 return outfile 2240 } 2241 2242 if !should { 2243 return outfile 2244 } 2245 2246 hadSlashSuffix := strings.HasSuffix(outfile, "/") 2247 2248 outfile = "/" + path.Join(p.Lang(), outfile) 2249 if hadSlashSuffix { 2250 outfile += "/" 2251 } 2252 return outfile 2253 } 2254 2255 func sectionsFromFile(fi *fileInfo) []string { 2256 dirname := fi.Dir() 2257 dirname = strings.Trim(dirname, helpers.FilePathSeparator) 2258 if dirname == "" { 2259 return nil 2260 } 2261 parts := strings.Split(dirname, helpers.FilePathSeparator) 2262 2263 if fi.bundleTp == bundleLeaf && len(parts) > 0 { 2264 // my-section/mybundle/index.md => my-section 2265 return parts[:len(parts)-1] 2266 } 2267 2268 return parts 2269 } 2270 2271 func kindFromFileInfo(fi *fileInfo) string { 2272 if fi.TranslationBaseName() == "_index" { 2273 if fi.Dir() == "" { 2274 return KindHome 2275 } 2276 // Could be index for section, taxonomy, taxonomy term 2277 // We don't know enough yet to determine which 2278 return kindUnknown 2279 } 2280 return KindPage 2281 } 2282 2283 func (p *Page) setValuesForKind(s *Site) { 2284 if p.Kind == kindUnknown { 2285 // This is either a taxonomy list, taxonomy term or a section 2286 nodeType := s.kindFromSections(p.sections) 2287 2288 if nodeType == kindUnknown { 2289 panic(fmt.Sprintf("Unable to determine page kind from %q", p.sections)) 2290 } 2291 2292 p.Kind = nodeType 2293 } 2294 2295 switch p.Kind { 2296 case KindHome: 2297 p.URLPath.URL = "/" 2298 case KindPage: 2299 default: 2300 if p.URLPath.URL == "" { 2301 p.URLPath.URL = "/" + path.Join(p.sections...) + "/" 2302 } 2303 } 2304 } 2305 2306 // Used in error logs. 2307 func (p *Page) pathOrTitle() string { 2308 if p.Path() != "" { 2309 return p.Path() 2310 } 2311 return p.title 2312 }