github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/page__meta.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 "fmt" 18 "path" 19 "path/filepath" 20 "regexp" 21 "strings" 22 "sync" 23 "time" 24 25 "github.com/gohugoio/hugo/langs" 26 27 "github.com/gobuffalo/flect" 28 "github.com/gohugoio/hugo/markup/converter" 29 30 "github.com/gohugoio/hugo/hugofs/files" 31 32 "github.com/gohugoio/hugo/common/hugo" 33 34 "github.com/gohugoio/hugo/related" 35 36 "github.com/gohugoio/hugo/source" 37 38 "github.com/gohugoio/hugo/common/maps" 39 "github.com/gohugoio/hugo/config" 40 "github.com/gohugoio/hugo/helpers" 41 42 "github.com/gohugoio/hugo/output" 43 "github.com/gohugoio/hugo/resources/page" 44 "github.com/gohugoio/hugo/resources/page/pagemeta" 45 "github.com/gohugoio/hugo/resources/resource" 46 "github.com/spf13/cast" 47 ) 48 49 var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) 50 51 type pageMeta struct { 52 // kind is the discriminator that identifies the different page types 53 // in the different page collections. This can, as an example, be used 54 // to to filter regular pages, find sections etc. 55 // Kind will, for the pages available to the templates, be one of: 56 // page, home, section, taxonomy and term. 57 // It is of string type to make it easy to reason about in 58 // the templates. 59 kind string 60 61 // This is a standalone page not part of any page collection. These 62 // include sitemap, robotsTXT and similar. It will have no pageOutputs, but 63 // a fixed pageOutput. 64 standalone bool 65 66 draft bool // Only published when running with -D flag 67 buildConfig pagemeta.BuildConfig 68 69 bundleType files.ContentClass 70 71 // Params contains configuration defined in the params section of page frontmatter. 72 params map[string]any 73 74 title string 75 linkTitle string 76 77 summary string 78 79 resourcePath string 80 81 weight int 82 83 markup string 84 contentType string 85 86 // whether the content is in a CJK language. 87 isCJKLanguage bool 88 89 layout string 90 91 aliases []string 92 93 description string 94 keywords []string 95 96 urlPaths pagemeta.URLPath 97 98 resource.Dates 99 100 // Set if this page is bundled inside another. 101 bundled bool 102 103 // A key that maps to translation(s) of this page. This value is fetched 104 // from the page front matter. 105 translationKey string 106 107 // From front matter. 108 configuredOutputFormats output.Formats 109 110 // This is the raw front matter metadata that is going to be assigned to 111 // the Resources above. 112 resourcesMetadata []map[string]any 113 114 f source.File 115 116 sections []string 117 118 // Sitemap overrides from front matter. 119 sitemap config.Sitemap 120 121 s *Site 122 123 contentConverterInit sync.Once 124 contentConverter converter.Converter 125 } 126 127 func (p *pageMeta) Aliases() []string { 128 return p.aliases 129 } 130 131 func (p *pageMeta) Author() page.Author { 132 helpers.Deprecated(".Author", "Use taxonomies.", false) 133 authors := p.Authors() 134 135 for _, author := range authors { 136 return author 137 } 138 return page.Author{} 139 } 140 141 func (p *pageMeta) Authors() page.AuthorList { 142 helpers.Deprecated(".Authors", "Use taxonomies.", false) 143 authorKeys, ok := p.params["authors"] 144 if !ok { 145 return page.AuthorList{} 146 } 147 authors := authorKeys.([]string) 148 if len(authors) < 1 || len(p.s.Info.Authors) < 1 { 149 return page.AuthorList{} 150 } 151 152 al := make(page.AuthorList) 153 for _, author := range authors { 154 a, ok := p.s.Info.Authors[author] 155 if ok { 156 al[author] = a 157 } 158 } 159 return al 160 } 161 162 func (p *pageMeta) BundleType() files.ContentClass { 163 return p.bundleType 164 } 165 166 func (p *pageMeta) Description() string { 167 return p.description 168 } 169 170 func (p *pageMeta) Lang() string { 171 return p.s.Lang() 172 } 173 174 func (p *pageMeta) Draft() bool { 175 return p.draft 176 } 177 178 func (p *pageMeta) File() source.File { 179 return p.f 180 } 181 182 func (p *pageMeta) IsHome() bool { 183 return p.Kind() == page.KindHome 184 } 185 186 func (p *pageMeta) Keywords() []string { 187 return p.keywords 188 } 189 190 func (p *pageMeta) Kind() string { 191 return p.kind 192 } 193 194 func (p *pageMeta) Layout() string { 195 return p.layout 196 } 197 198 func (p *pageMeta) LinkTitle() string { 199 if p.linkTitle != "" { 200 return p.linkTitle 201 } 202 203 return p.Title() 204 } 205 206 func (p *pageMeta) Name() string { 207 if p.resourcePath != "" { 208 return p.resourcePath 209 } 210 return p.Title() 211 } 212 213 func (p *pageMeta) IsNode() bool { 214 return !p.IsPage() 215 } 216 217 func (p *pageMeta) IsPage() bool { 218 return p.Kind() == page.KindPage 219 } 220 221 // Param is a convenience method to do lookups in Page's and Site's Params map, 222 // in that order. 223 // 224 // This method is also implemented on SiteInfo. 225 // TODO(bep) interface 226 func (p *pageMeta) Param(key any) (any, error) { 227 return resource.Param(p, p.s.Info.Params(), key) 228 } 229 230 func (p *pageMeta) Params() maps.Params { 231 return p.params 232 } 233 234 func (p *pageMeta) Path() string { 235 if !p.File().IsZero() { 236 const example = ` 237 {{ $path := "" }} 238 {{ with .File }} 239 {{ $path = .Path }} 240 {{ else }} 241 {{ $path = .Path }} 242 {{ end }} 243 ` 244 helpers.Deprecated(".Path when the page is backed by a file", "We plan to use Path for a canonical source path and you probably want to check the source is a file. To get the current behaviour, you can use a construct similar to the one below:\n"+example, false) 245 246 } 247 248 return p.Pathc() 249 } 250 251 // This is just a bridge method, use Path in templates. 252 func (p *pageMeta) Pathc() string { 253 if !p.File().IsZero() { 254 return p.File().Path() 255 } 256 return p.SectionsPath() 257 } 258 259 // RelatedKeywords implements the related.Document interface needed for fast page searches. 260 func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { 261 v, err := p.Param(cfg.Name) 262 if err != nil { 263 return nil, err 264 } 265 266 return cfg.ToKeywords(v) 267 } 268 269 func (p *pageMeta) IsSection() bool { 270 return p.Kind() == page.KindSection 271 } 272 273 func (p *pageMeta) Section() string { 274 if p.IsHome() { 275 return "" 276 } 277 278 if p.IsNode() { 279 if len(p.sections) == 0 { 280 // May be a sitemap or similar. 281 return "" 282 } 283 return p.sections[0] 284 } 285 286 if !p.File().IsZero() { 287 return p.File().Section() 288 } 289 290 panic("invalid page state") 291 } 292 293 func (p *pageMeta) SectionsEntries() []string { 294 return p.sections 295 } 296 297 func (p *pageMeta) SectionsPath() string { 298 return path.Join(p.SectionsEntries()...) 299 } 300 301 func (p *pageMeta) Sitemap() config.Sitemap { 302 return p.sitemap 303 } 304 305 func (p *pageMeta) Title() string { 306 return p.title 307 } 308 309 const defaultContentType = "page" 310 311 func (p *pageMeta) Type() string { 312 if p.contentType != "" { 313 return p.contentType 314 } 315 316 if sect := p.Section(); sect != "" { 317 return sect 318 } 319 320 return defaultContentType 321 } 322 323 func (p *pageMeta) Weight() int { 324 return p.weight 325 } 326 327 func (pm *pageMeta) mergeBucketCascades(b1, b2 *pagesMapBucket) { 328 if b1.cascade == nil { 329 b1.cascade = make(map[page.PageMatcher]maps.Params) 330 } 331 332 if b2 != nil && b2.cascade != nil { 333 for k, v := range b2.cascade { 334 335 vv, found := b1.cascade[k] 336 if !found { 337 b1.cascade[k] = v 338 } else { 339 // Merge 340 for ck, cv := range v { 341 if _, found := vv[ck]; !found { 342 vv[ck] = cv 343 } 344 } 345 } 346 } 347 } 348 } 349 350 func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, frontmatter map[string]any) error { 351 pm.params = make(maps.Params) 352 353 if frontmatter == nil && (parentBucket == nil || parentBucket.cascade == nil) { 354 return nil 355 } 356 357 if frontmatter != nil { 358 // Needed for case insensitive fetching of params values 359 maps.PrepareParams(frontmatter) 360 if p.bucket != nil { 361 // Check for any cascade define on itself. 362 if cv, found := frontmatter["cascade"]; found { 363 var err error 364 p.bucket.cascade, err = page.DecodeCascade(cv) 365 if err != nil { 366 return err 367 } 368 } 369 } 370 } else { 371 frontmatter = make(map[string]any) 372 } 373 374 var cascade map[page.PageMatcher]maps.Params 375 376 if p.bucket != nil { 377 if parentBucket != nil { 378 // Merge missing keys from parent into this. 379 pm.mergeBucketCascades(p.bucket, parentBucket) 380 } 381 cascade = p.bucket.cascade 382 } else if parentBucket != nil { 383 cascade = parentBucket.cascade 384 } 385 386 for m, v := range cascade { 387 if !m.Matches(p) { 388 continue 389 } 390 for kk, vv := range v { 391 if _, found := frontmatter[kk]; !found { 392 frontmatter[kk] = vv 393 } 394 } 395 } 396 397 var mtime time.Time 398 var contentBaseName string 399 if !p.File().IsZero() { 400 contentBaseName = p.File().ContentBaseName() 401 if p.File().FileInfo() != nil { 402 mtime = p.File().FileInfo().ModTime() 403 } 404 } 405 406 var gitAuthorDate time.Time 407 if !p.gitInfo.IsZero() { 408 gitAuthorDate = p.gitInfo.AuthorDate 409 } 410 411 descriptor := &pagemeta.FrontMatterDescriptor{ 412 Frontmatter: frontmatter, 413 Params: pm.params, 414 Dates: &pm.Dates, 415 PageURLs: &pm.urlPaths, 416 BaseFilename: contentBaseName, 417 ModTime: mtime, 418 GitAuthorDate: gitAuthorDate, 419 Location: langs.GetLocation(pm.s.Language()), 420 } 421 422 // Handle the date separately 423 // TODO(bep) we need to "do more" in this area so this can be split up and 424 // more easily tested without the Page, but the coupling is strong. 425 err := pm.s.frontmatterHandler.HandleDates(descriptor) 426 if err != nil { 427 p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) 428 } 429 430 pm.buildConfig, err = pagemeta.DecodeBuildConfig(frontmatter["_build"]) 431 if err != nil { 432 return err 433 } 434 435 var sitemapSet bool 436 437 var draft, published, isCJKLanguage *bool 438 for k, v := range frontmatter { 439 loki := strings.ToLower(k) 440 441 if loki == "published" { // Intentionally undocumented 442 vv, err := cast.ToBoolE(v) 443 if err == nil { 444 published = &vv 445 } 446 // published may also be a date 447 continue 448 } 449 450 if pm.s.frontmatterHandler.IsDateKey(loki) { 451 continue 452 } 453 454 switch loki { 455 case "title": 456 pm.title = cast.ToString(v) 457 pm.params[loki] = pm.title 458 case "linktitle": 459 pm.linkTitle = cast.ToString(v) 460 pm.params[loki] = pm.linkTitle 461 case "summary": 462 pm.summary = cast.ToString(v) 463 pm.params[loki] = pm.summary 464 case "description": 465 pm.description = cast.ToString(v) 466 pm.params[loki] = pm.description 467 case "slug": 468 // Don't start or end with a - 469 pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-") 470 pm.params[loki] = pm.Slug() 471 case "url": 472 url := cast.ToString(v) 473 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { 474 return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) 475 } 476 lang := p.s.GetLanguagePrefix() 477 if lang != "" && !strings.HasPrefix(url, "/") && strings.HasPrefix(url, lang+"/") { 478 if strings.HasPrefix(hugo.CurrentVersion.String(), "0.55") { 479 // We added support for page relative URLs in Hugo 0.55 and 480 // this may get its language path added twice. 481 // TODO(bep) eventually remove this. 482 p.s.Log.Warnf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url) 483 } 484 } 485 pm.urlPaths.URL = url 486 pm.params[loki] = url 487 case "type": 488 pm.contentType = cast.ToString(v) 489 pm.params[loki] = pm.contentType 490 case "keywords": 491 pm.keywords = cast.ToStringSlice(v) 492 pm.params[loki] = pm.keywords 493 case "headless": 494 // Legacy setting for leaf bundles. 495 // This is since Hugo 0.63 handled in a more general way for all 496 // pages. 497 isHeadless := cast.ToBool(v) 498 pm.params[loki] = isHeadless 499 if p.File().TranslationBaseName() == "index" && isHeadless { 500 pm.buildConfig.List = pagemeta.Never 501 pm.buildConfig.Render = pagemeta.Never 502 } 503 case "outputs": 504 o := cast.ToStringSlice(v) 505 if len(o) > 0 { 506 // Output formats are explicitly set in front matter, use those. 507 outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) 508 509 if err != nil { 510 p.s.Log.Errorf("Failed to resolve output formats: %s", err) 511 } else { 512 pm.configuredOutputFormats = outFormats 513 pm.params[loki] = outFormats 514 } 515 516 } 517 case "draft": 518 draft = new(bool) 519 *draft = cast.ToBool(v) 520 case "layout": 521 pm.layout = cast.ToString(v) 522 pm.params[loki] = pm.layout 523 case "markup": 524 pm.markup = cast.ToString(v) 525 pm.params[loki] = pm.markup 526 case "weight": 527 pm.weight = cast.ToInt(v) 528 pm.params[loki] = pm.weight 529 case "aliases": 530 pm.aliases = cast.ToStringSlice(v) 531 for i, alias := range pm.aliases { 532 if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { 533 return fmt.Errorf("http* aliases not supported: %q", alias) 534 } 535 pm.aliases[i] = filepath.ToSlash(alias) 536 } 537 pm.params[loki] = pm.aliases 538 case "sitemap": 539 p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, maps.ToStringMap(v)) 540 pm.params[loki] = p.m.sitemap 541 sitemapSet = true 542 case "iscjklanguage": 543 isCJKLanguage = new(bool) 544 *isCJKLanguage = cast.ToBool(v) 545 case "translationkey": 546 pm.translationKey = cast.ToString(v) 547 pm.params[loki] = pm.translationKey 548 case "resources": 549 var resources []map[string]any 550 handled := true 551 552 switch vv := v.(type) { 553 case []map[any]any: 554 for _, vvv := range vv { 555 resources = append(resources, maps.ToStringMap(vvv)) 556 } 557 case []map[string]any: 558 resources = append(resources, vv...) 559 case []any: 560 for _, vvv := range vv { 561 switch vvvv := vvv.(type) { 562 case map[any]any: 563 resources = append(resources, maps.ToStringMap(vvvv)) 564 case map[string]any: 565 resources = append(resources, vvvv) 566 } 567 } 568 default: 569 handled = false 570 } 571 572 if handled { 573 pm.params[loki] = resources 574 pm.resourcesMetadata = resources 575 break 576 } 577 fallthrough 578 579 default: 580 // If not one of the explicit values, store in Params 581 switch vv := v.(type) { 582 case []any: 583 if len(vv) > 0 { 584 allStrings := true 585 for _, vvv := range vv { 586 if _, ok := vvv.(string); !ok { 587 allStrings = false 588 break 589 } 590 } 591 if allStrings { 592 // We need tags, keywords etc. to be []string, not []interface{}. 593 a := make([]string, len(vv)) 594 for i, u := range vv { 595 a[i] = cast.ToString(u) 596 } 597 pm.params[loki] = a 598 } else { 599 pm.params[loki] = vv 600 } 601 } else { 602 pm.params[loki] = []string{} 603 } 604 default: 605 pm.params[loki] = vv 606 } 607 } 608 } 609 610 if !sitemapSet { 611 pm.sitemap = p.s.siteCfg.sitemap 612 } 613 614 pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup) 615 616 if draft != nil && published != nil { 617 pm.draft = *draft 618 p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) 619 } else if draft != nil { 620 pm.draft = *draft 621 } else if published != nil { 622 pm.draft = !*published 623 } 624 pm.params["draft"] = pm.draft 625 626 if isCJKLanguage != nil { 627 pm.isCJKLanguage = *isCJKLanguage 628 } else if p.s.siteCfg.hasCJKLanguage && p.source.parsed != nil { 629 if cjkRe.Match(p.source.parsed.Input()) { 630 pm.isCJKLanguage = true 631 } else { 632 pm.isCJKLanguage = false 633 } 634 } 635 636 pm.params["iscjklanguage"] = p.m.isCJKLanguage 637 638 return nil 639 } 640 641 func (p *pageMeta) noListAlways() bool { 642 return p.buildConfig.List != pagemeta.Always 643 } 644 645 func (p *pageMeta) getListFilter(local bool) contentTreeNodeCallback { 646 return newContentTreeFilter(func(n *contentNode) bool { 647 if n == nil { 648 return true 649 } 650 651 var shouldList bool 652 switch n.p.m.buildConfig.List { 653 case pagemeta.Always: 654 shouldList = true 655 case pagemeta.Never: 656 shouldList = false 657 case pagemeta.ListLocally: 658 shouldList = local 659 } 660 661 return !shouldList 662 }) 663 } 664 665 func (p *pageMeta) noRender() bool { 666 return p.buildConfig.Render != pagemeta.Always 667 } 668 669 func (p *pageMeta) noLink() bool { 670 return p.buildConfig.Render == pagemeta.Never 671 } 672 673 func (p *pageMeta) applyDefaultValues(n *contentNode) error { 674 if p.buildConfig.IsZero() { 675 p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil) 676 } 677 678 if !p.s.isEnabled(p.Kind()) { 679 (&p.buildConfig).Disable() 680 } 681 682 if p.markup == "" { 683 if !p.File().IsZero() { 684 // Fall back to file extension 685 p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) 686 } 687 if p.markup == "" { 688 p.markup = "markdown" 689 } 690 } 691 692 if p.title == "" && p.f.IsZero() { 693 switch p.Kind() { 694 case page.KindHome: 695 p.title = p.s.Info.title 696 case page.KindSection: 697 var sectionName string 698 if n != nil { 699 sectionName = n.rootSection() 700 } else { 701 sectionName = p.sections[0] 702 } 703 704 sectionName = helpers.FirstUpper(sectionName) 705 if p.s.Cfg.GetBool("pluralizeListTitles") { 706 p.title = flect.Pluralize(sectionName) 707 } else { 708 p.title = sectionName 709 } 710 case page.KindTerm: 711 // TODO(bep) improve 712 key := p.sections[len(p.sections)-1] 713 p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1) 714 case page.KindTaxonomy: 715 p.title = p.s.titleFunc(p.sections[0]) 716 case kind404: 717 p.title = "404 Page not found" 718 719 } 720 } 721 722 if p.IsNode() { 723 p.bundleType = files.ContentClassBranch 724 } else { 725 source := p.File() 726 if fi, ok := source.(*fileInfo); ok { 727 class := fi.FileInfo().Meta().Classifier 728 switch class { 729 case files.ContentClassBranch, files.ContentClassLeaf: 730 p.bundleType = class 731 } 732 } 733 } 734 735 return nil 736 } 737 738 func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) { 739 if ps == nil { 740 panic("no Page provided") 741 } 742 cp := p.s.ContentSpec.Converters.Get(markup) 743 if cp == nil { 744 return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q", p.markup) 745 } 746 747 var id string 748 var filename string 749 var path string 750 if !p.f.IsZero() { 751 id = p.f.UniqueID() 752 filename = p.f.Filename() 753 path = p.f.Path() 754 } else { 755 path = p.Pathc() 756 } 757 758 cpp, err := cp.New( 759 converter.DocumentContext{ 760 Document: newPageForRenderHook(ps), 761 DocumentID: id, 762 DocumentName: path, 763 Filename: filename, 764 }, 765 ) 766 if err != nil { 767 return converter.NopConverter, err 768 } 769 770 return cpp, nil 771 } 772 773 // The output formats this page will be rendered to. 774 func (m *pageMeta) outputFormats() output.Formats { 775 if len(m.configuredOutputFormats) > 0 { 776 return m.configuredOutputFormats 777 } 778 779 return m.s.outputFormats[m.Kind()] 780 } 781 782 func (p *pageMeta) Slug() string { 783 return p.urlPaths.Slug 784 } 785 786 func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { 787 v := m.Params()[strings.ToLower(key)] 788 789 if v == nil { 790 return nil 791 } 792 793 switch val := v.(type) { 794 case bool: 795 return val 796 case string: 797 if stringToLower { 798 return strings.ToLower(val) 799 } 800 return val 801 case int64, int32, int16, int8, int: 802 return cast.ToInt(v) 803 case float64, float32: 804 return cast.ToFloat64(v) 805 case time.Time: 806 return val 807 case []string: 808 if stringToLower { 809 return helpers.SliceToLower(val) 810 } 811 return v 812 default: 813 return v 814 } 815 } 816 817 func getParamToLower(m resource.ResourceParamsProvider, key string) any { 818 return getParam(m, key, true) 819 }