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