github.com/neohugo/neohugo@v0.123.8/tpl/tplimpl/template.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 tplimpl 15 16 import ( 17 "bytes" 18 "context" 19 "embed" 20 "fmt" 21 "io" 22 "io/fs" 23 "path/filepath" 24 "reflect" 25 "regexp" 26 "sort" 27 "strings" 28 "sync" 29 "time" 30 "unicode" 31 "unicode/utf8" 32 33 "github.com/neohugo/neohugo/common/types" 34 "github.com/neohugo/neohugo/output/layouts" 35 36 "github.com/neohugo/neohugo/helpers" 37 38 "github.com/neohugo/neohugo/output" 39 40 "github.com/neohugo/neohugo/deps" 41 "github.com/spf13/afero" 42 43 "github.com/neohugo/neohugo/common/herrors" 44 "github.com/neohugo/neohugo/hugofs" 45 46 htmltemplate "github.com/neohugo/neohugo/tpl/internal/go_templates/htmltemplate" 47 texttemplate "github.com/neohugo/neohugo/tpl/internal/go_templates/texttemplate" 48 49 "github.com/neohugo/neohugo/identity" 50 "github.com/neohugo/neohugo/tpl" 51 ) 52 53 const ( 54 textTmplNamePrefix = "_text/" 55 56 shortcodesPathPrefix = "shortcodes/" 57 internalPathPrefix = "_internal/" 58 embeddedPathPrefix = "_embedded/" 59 baseFileBase = "baseof" 60 ) 61 62 // The identifiers may be truncated in the log, e.g. 63 // "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image" 64 // We need this to identify position in templates with base templates applied. 65 var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`) 66 67 var embeddedTemplatesAliases = map[string][]string{ 68 "shortcodes/twitter.html": {"shortcodes/tweet.html"}, 69 } 70 71 var ( 72 _ tpl.TemplateManager = (*templateExec)(nil) 73 _ tpl.TemplateHandler = (*templateExec)(nil) 74 _ tpl.TemplateFuncGetter = (*templateExec)(nil) 75 _ tpl.TemplateFinder = (*templateExec)(nil) 76 _ tpl.UnusedTemplatesProvider = (*templateExec)(nil) 77 78 _ tpl.Template = (*templateState)(nil) 79 _ tpl.Info = (*templateState)(nil) 80 ) 81 82 var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`) 83 84 // needsBaseTemplate returns true if the first non-comment template block is a 85 // define block. 86 // If a base template does not exist, we will handle that when it's used. 87 func needsBaseTemplate(templ string) bool { 88 idx := -1 89 inComment := false 90 for i := 0; i < len(templ); { 91 if !inComment && strings.HasPrefix(templ[i:], "{{/*") { 92 inComment = true 93 i += 4 94 } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") { 95 inComment = true 96 i += 6 97 } else if inComment && strings.HasPrefix(templ[i:], "*/}}") { 98 inComment = false 99 i += 4 100 } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") { 101 inComment = false 102 i += 6 103 } else { 104 r, size := utf8.DecodeRuneInString(templ[i:]) 105 if !inComment { 106 if strings.HasPrefix(templ[i:], "{{") { 107 idx = i 108 break 109 } else if !unicode.IsSpace(r) { 110 break 111 } 112 } 113 i += size 114 } 115 } 116 117 if idx == -1 { 118 return false 119 } 120 121 return baseTemplateDefineRe.MatchString(templ[idx:]) 122 } 123 124 func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder { 125 return &textTemplateWrapperWithLock{ 126 RWMutex: &sync.RWMutex{}, 127 Template: texttemplate.New("").Funcs(funcs), 128 } 129 } 130 131 func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) { 132 exec, funcs := newTemplateExecuter(d) 133 funcMap := make(map[string]any) 134 for k, v := range funcs { 135 funcMap[k] = v.Interface() 136 } 137 138 var templateUsageTracker map[string]templateInfo 139 if d.Conf.PrintUnusedTemplates() { 140 templateUsageTracker = make(map[string]templateInfo) 141 } 142 143 h := &templateHandler{ 144 nameBaseTemplateName: make(map[string]string), 145 transformNotFound: make(map[string]*templateState), 146 147 shortcodes: make(map[string]*shortcodeTemplates), 148 templateInfo: make(map[string]tpl.Info), 149 baseof: make(map[string]templateInfo), 150 needsBaseof: make(map[string]templateInfo), 151 152 main: newTemplateNamespace(funcMap), 153 154 Deps: d, 155 layoutHandler: layouts.NewLayoutHandler(), 156 layoutsFs: d.BaseFs.Layouts.Fs, 157 layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry), 158 159 templateUsageTracker: templateUsageTracker, 160 } 161 162 if err := h.loadEmbedded(); err != nil { 163 return nil, err 164 } 165 166 if err := h.loadTemplates(); err != nil { 167 return nil, err 168 } 169 170 e := &templateExec{ 171 d: d, 172 executor: exec, 173 funcs: funcs, 174 templateHandler: h, 175 } 176 177 if err := e.postTransform(); err != nil { 178 return nil, err 179 } 180 181 return &tpl.TemplateHandlers{ 182 Tmpl: e, 183 TxtTmpl: newStandaloneTextTemplate(funcMap), 184 }, nil 185 } 186 187 func newTemplateNamespace(funcs map[string]any) *templateNamespace { 188 return &templateNamespace{ 189 prototypeHTML: htmltemplate.New("").Funcs(funcs), 190 prototypeText: texttemplate.New("").Funcs(funcs), 191 templateStateMap: &templateStateMap{ 192 templates: make(map[string]*templateState), 193 }, 194 } 195 } 196 197 func newTemplateState(templ tpl.Template, info templateInfo, id identity.Identity) *templateState { 198 if id == nil { 199 id = info 200 } 201 return &templateState{ 202 info: info, 203 typ: info.resolveType(), 204 Template: templ, 205 parseInfo: tpl.DefaultParseInfo, 206 id: id, 207 } 208 } 209 210 type layoutCacheKey struct { 211 d layouts.LayoutDescriptor 212 f string 213 } 214 215 type templateExec struct { 216 d *deps.Deps 217 executor texttemplate.Executer 218 funcs map[string]reflect.Value 219 220 *templateHandler 221 } 222 223 func (t templateExec) Clone(d *deps.Deps) *templateExec { 224 exec, funcs := newTemplateExecuter(d) 225 t.executor = exec 226 t.funcs = funcs 227 t.d = d 228 return &t 229 } 230 231 func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error { 232 return t.ExecuteWithContext(context.Background(), templ, wr, data) 233 } 234 235 func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error { 236 if rlocker, ok := templ.(types.RLocker); ok { 237 rlocker.RLock() 238 defer rlocker.RUnlock() 239 } 240 if t.Metrics != nil { 241 defer t.Metrics.MeasureSince(templ.Name(), time.Now()) 242 } 243 244 if t.templateUsageTracker != nil { 245 if ts, ok := templ.(*templateState); ok { 246 247 t.templateUsageTrackerMu.Lock() 248 if _, found := t.templateUsageTracker[ts.Name()]; !found { 249 t.templateUsageTracker[ts.Name()] = ts.info 250 } 251 252 if !ts.baseInfo.IsZero() { 253 if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found { 254 t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo 255 } 256 } 257 t.templateUsageTrackerMu.Unlock() 258 } 259 } 260 261 execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data) 262 if execErr != nil { 263 execErr = t.addFileContext(templ, execErr) 264 } 265 return execErr 266 } 267 268 func (t *templateExec) UnusedTemplates() []tpl.FileInfo { 269 if t.templateUsageTracker == nil { 270 return nil 271 } 272 var unused []tpl.FileInfo 273 274 for _, ti := range t.needsBaseof { 275 if _, found := t.templateUsageTracker[ti.name]; !found { 276 unused = append(unused, ti) 277 } 278 } 279 280 for _, ti := range t.baseof { 281 if _, found := t.templateUsageTracker[ti.name]; !found { 282 unused = append(unused, ti) 283 } 284 } 285 286 for _, ts := range t.main.templates { 287 ti := ts.info 288 if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil { 289 continue 290 } 291 292 if _, found := t.templateUsageTracker[ti.name]; !found { 293 unused = append(unused, ti) 294 } 295 } 296 297 sort.Slice(unused, func(i, j int) bool { 298 return unused[i].Name() < unused[j].Name() 299 }) 300 301 return unused 302 } 303 304 func (t *templateExec) GetFunc(name string) (reflect.Value, bool) { 305 v, found := t.funcs[name] 306 return v, found 307 } 308 309 func (t *templateExec) MarkReady() error { 310 var err error 311 t.readyInit.Do(func() { 312 // We only need the clones if base templates are in use. 313 if len(t.needsBaseof) > 0 { 314 err = t.main.createPrototypes() 315 } 316 }) 317 318 return err 319 } 320 321 type templateHandler struct { 322 main *templateNamespace 323 needsBaseof map[string]templateInfo 324 baseof map[string]templateInfo 325 326 readyInit sync.Once //nolint 327 328 // This is the filesystem to load the templates from. All the templates are 329 // stored in the root of this filesystem. 330 layoutsFs afero.Fs 331 332 layoutHandler *layouts.LayoutHandler 333 334 layoutTemplateCache map[layoutCacheKey]layoutCacheEntry 335 layoutTemplateCacheMu sync.RWMutex 336 337 *deps.Deps 338 339 // Used to get proper filenames in errors 340 nameBaseTemplateName map[string]string 341 342 // Holds name and source of template definitions not found during the first 343 // AST transformation pass. 344 transformNotFound map[string]*templateState 345 346 // shortcodes maps shortcode name to template variants 347 // (language, output format etc.) of that shortcode. 348 shortcodes map[string]*shortcodeTemplates 349 350 // templateInfo maps template name to some additional information about that template. 351 // Note that for shortcodes that same information is embedded in the 352 // shortcodeTemplates type. 353 templateInfo map[string]tpl.Info 354 355 // May be nil. 356 templateUsageTracker map[string]templateInfo 357 templateUsageTrackerMu sync.Mutex //nolint 358 } 359 360 type layoutCacheEntry struct { 361 found bool 362 templ tpl.Template 363 err error 364 } 365 366 // AddTemplate parses and adds a template to the collection. 367 // Templates with name prefixed with "_text" will be handled as plain 368 // text templates. 369 func (t *templateHandler) AddTemplate(name, tpl string) error { 370 templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main) 371 if err == nil { 372 _, err = t.applyTemplateTransformers(t.main, templ) 373 } 374 return err 375 } 376 377 func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { 378 templ, found := t.main.Lookup(name) 379 if found { 380 return templ, true 381 } 382 383 return nil, false 384 } 385 386 func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { 387 key := layoutCacheKey{d, f.Name} 388 t.layoutTemplateCacheMu.RLock() 389 if cacheVal, found := t.layoutTemplateCache[key]; found { 390 t.layoutTemplateCacheMu.RUnlock() 391 return cacheVal.templ, cacheVal.found, cacheVal.err 392 } 393 t.layoutTemplateCacheMu.RUnlock() 394 395 t.layoutTemplateCacheMu.Lock() 396 defer t.layoutTemplateCacheMu.Unlock() 397 398 templ, found, err := t.findLayout(d, f) 399 cacheVal := layoutCacheEntry{found: found, templ: templ, err: err} 400 t.layoutTemplateCache[key] = cacheVal 401 return cacheVal.templ, cacheVal.found, cacheVal.err 402 } 403 404 // This currently only applies to shortcodes and what we get here is the 405 // shortcode name. 406 func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { 407 name = templateBaseName(templateShortcode, name) 408 s, found := t.shortcodes[name] 409 if !found { 410 return nil, false, false 411 } 412 413 sv, found := s.fromVariants(variants) 414 if !found { 415 return nil, false, false 416 } 417 418 more := len(s.variants) > 1 419 420 return sv.ts, true, more 421 } 422 423 // LookupVariants returns all variants of name, nil if none found. 424 func (t *templateHandler) LookupVariants(name string) []tpl.Template { 425 name = templateBaseName(templateShortcode, name) 426 s, found := t.shortcodes[name] 427 if !found { 428 return nil 429 } 430 431 variants := make([]tpl.Template, len(s.variants)) 432 for i := 0; i < len(variants); i++ { 433 variants[i] = s.variants[i].ts 434 } 435 436 return variants 437 } 438 439 func (t *templateHandler) HasTemplate(name string) bool { 440 if _, found := t.baseof[name]; found { 441 return true 442 } 443 444 if _, found := t.needsBaseof[name]; found { 445 return true 446 } 447 448 _, found := t.Lookup(name) 449 return found 450 } 451 452 func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) { 453 if _, found := t.needsBaseof[name]; found { 454 return identity.StringIdentity(name), true 455 } 456 457 if _, found := t.baseof[name]; found { 458 return identity.StringIdentity(name), true 459 } 460 461 tt, found := t.Lookup(name) 462 if !found { 463 return nil, false 464 } 465 return tt.(identity.IdentityProvider).GetIdentity(), found 466 } 467 468 func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { 469 d.OutputFormatName = f.Name 470 d.Suffix = f.MediaType.FirstSuffix.Suffix 471 layouts, _ := t.layoutHandler.For(d) 472 for _, name := range layouts { 473 templ, found := t.main.Lookup(name) 474 if found { 475 return templ, true, nil 476 } 477 478 overlay, found := t.needsBaseof[name] 479 480 if !found { 481 continue 482 } 483 484 d.Baseof = true 485 baseLayouts, _ := t.layoutHandler.For(d) 486 var base templateInfo 487 found = false 488 for _, l := range baseLayouts { 489 base, found = t.baseof[l] 490 if found { 491 break 492 } 493 } 494 495 templ, err := t.applyBaseTemplate(overlay, base) 496 if err != nil { 497 return nil, false, err 498 } 499 500 ts := newTemplateState(templ, overlay, identity.Or(base, overlay)) 501 502 if found { 503 ts.baseInfo = base 504 } 505 506 if _, err := t.applyTemplateTransformers(t.main, ts); err != nil { 507 return nil, false, err 508 } 509 510 if err := t.extractPartials(ts.Template); err != nil { 511 return nil, false, err 512 } 513 514 return ts, true, nil 515 516 } 517 518 return nil, false, nil 519 } 520 521 func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo { 522 var isText bool 523 var isEmbedded bool 524 525 if strings.HasPrefix(name, embeddedPathPrefix) { 526 isEmbedded = true 527 name = strings.TrimPrefix(name, embeddedPathPrefix) 528 } 529 530 name, isText = t.nameIsText(name) 531 return templateInfo{ 532 name: name, 533 isText: isText, 534 isEmbedded: isEmbedded, 535 template: tpl, 536 } 537 } 538 539 func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error { 540 if strings.HasPrefix(templ.Name(), "_internal") { 541 return inerr 542 } 543 544 ts, ok := templ.(*templateState) 545 if !ok { 546 return inerr 547 } 548 549 identifiers := t.extractIdentifiers(inerr.Error()) 550 551 checkFilename := func(info templateInfo, inErr error) (error, bool) { 552 if info.meta == nil { 553 return inErr, false 554 } 555 556 lineMatcher := func(m herrors.LineMatcher) int { 557 if m.Position.LineNumber != m.LineNumber { 558 return -1 559 } 560 561 for _, id := range identifiers { 562 if strings.Contains(m.Line, id) { 563 // We found the line, but return a 0 to signal to 564 // use the column from the error message. 565 return 0 566 } 567 } 568 return -1 569 } 570 571 f, err := info.meta.Open() 572 if err != nil { 573 return inErr, false 574 } 575 defer f.Close() 576 577 fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename) 578 fe.UpdateContent(f, lineMatcher) // nolint 579 580 if !fe.ErrorContext().Position.IsValid() { 581 return inErr, false 582 } 583 return fe, true 584 } 585 586 inerr = fmt.Errorf("execute of template failed: %w", inerr) 587 588 if err, ok := checkFilename(ts.info, inerr); ok { 589 return err 590 } 591 592 err, _ := checkFilename(ts.baseInfo, inerr) 593 594 return err 595 } 596 597 func (t *templateHandler) extractIdentifiers(line string) []string { 598 m := identifiersRe.FindAllStringSubmatch(line, -1) 599 identifiers := make([]string, len(m)) 600 for i := 0; i < len(m); i++ { 601 identifiers[i] = m[i][1] 602 } 603 return identifiers 604 } 605 606 func (t *templateHandler) addShortcodeVariant(ts *templateState) { 607 name := ts.Name() 608 base := templateBaseName(templateShortcode, name) 609 610 shortcodename, variants := templateNameAndVariants(base) 611 612 templs, found := t.shortcodes[shortcodename] 613 if !found { 614 templs = &shortcodeTemplates{} 615 t.shortcodes[shortcodename] = templs 616 } 617 618 sv := shortcodeVariant{variants: variants, ts: ts} 619 620 i := templs.indexOf(variants) 621 622 if i != -1 { 623 // Only replace if it's an override of an internal template. 624 if !isInternal(name) { 625 templs.variants[i] = sv 626 } 627 } else { 628 templs.variants = append(templs.variants, sv) 629 } 630 } 631 632 func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error { 633 getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) { 634 meta := fim.Meta() 635 f, err := meta.Open() 636 if err != nil { 637 return templateInfo{meta: meta}, err 638 } 639 defer f.Close() 640 b, err := io.ReadAll(f) 641 if err != nil { 642 return templateInfo{meta: meta}, err 643 } 644 645 s := removeLeadingBOM(string(b)) 646 647 var isText bool 648 name, isText = t.nameIsText(name) 649 650 return templateInfo{ 651 name: name, 652 isText: isText, 653 template: s, 654 meta: meta, 655 }, nil 656 } 657 658 tinfo, err := getTemplate(fim) 659 if err != nil { 660 return err 661 } 662 663 if isBaseTemplatePath(name) { 664 // Store it for later. 665 t.baseof[name] = tinfo 666 return nil 667 } 668 669 needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template) 670 if needsBaseof { 671 t.needsBaseof[name] = tinfo 672 return nil 673 } 674 675 templ, err := t.addTemplateTo(tinfo, t.main) 676 if err != nil { 677 return tinfo.errWithFileContext("parse failed", err) //nolint 678 } 679 _, err = t.applyTemplateTransformers(t.main, templ) 680 if err != nil { 681 return err 682 } 683 684 return nil 685 } 686 687 func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) { 688 return to.parse(info) 689 } 690 691 func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) { 692 if overlay.isText { 693 var ( 694 templ = t.main.prototypeTextClone.New(overlay.name) 695 err error 696 ) 697 698 if !base.IsZero() { 699 templ, err = templ.Parse(base.template) 700 if err != nil { 701 return nil, base.errWithFileContext("parse failed", err) 702 } 703 } 704 705 templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template) 706 if err != nil { 707 return nil, overlay.errWithFileContext("parse failed", err) 708 } 709 710 // The extra lookup is a workaround, see 711 // * https://github.com/golang/go/issues/16101 712 // * https://github.com/gohugoio/hugo/issues/2549 713 templ = templ.Lookup(templ.Name()) 714 715 return templ, nil 716 } 717 718 var ( 719 templ = t.main.prototypeHTMLClone.New(overlay.name) 720 err error 721 ) 722 723 if !base.IsZero() { 724 templ, err = templ.Parse(base.template) 725 if err != nil { 726 return nil, base.errWithFileContext("parse failed", err) 727 } 728 } 729 730 templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template) 731 if err != nil { 732 return nil, overlay.errWithFileContext("parse failed", err) 733 } 734 735 // The extra lookup is a workaround, see 736 // * https://github.com/golang/go/issues/16101 737 // * https://github.com/neohugo/neohugo/issues/2549 738 // templ = templ.Lookup(templ.Name()) 739 740 return templ, err 741 } 742 743 func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) { 744 c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts)) 745 if err != nil { 746 return nil, err 747 } 748 749 for k := range c.templateNotFound { 750 t.transformNotFound[k] = ts 751 } 752 753 return c, err 754 } 755 756 //go:embed all:embedded/templates/* 757 //go:embed embedded/templates/_default/* 758 //go:embed embedded/templates/_server/* 759 var embeddedTemplatesFs embed.FS 760 761 func (t *templateHandler) loadEmbedded() error { 762 //nolint 763 return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error { 764 if err != nil { 765 return err 766 } 767 if d == nil || d.IsDir() { 768 return nil 769 } 770 771 //nolint 772 templb, err := embeddedTemplatesFs.ReadFile(path) 773 if err != nil { 774 return err 775 } 776 777 // Get the newlines on Windows in line with how we had it back when we used Go Generate 778 // to write the templates to Go files. 779 templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n"))) 780 name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/") 781 templateName := name 782 783 // For the render hooks and the server templates it does not make sense to preserve the 784 // double _internal double book-keeping, 785 // just add it if its now provided by the user. 786 if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") { 787 templateName = internalPathPrefix + name 788 } 789 790 if _, found := t.Lookup(templateName); !found { 791 if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil { 792 return err 793 } 794 } 795 796 if aliases, found := embeddedTemplatesAliases[name]; found { 797 // TODO(bep) avoid reparsing these aliases 798 for _, alias := range aliases { 799 alias = internalPathPrefix + alias 800 if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil { 801 return err 802 } 803 } 804 } 805 806 return nil 807 }) 808 } 809 810 func (t *templateHandler) loadTemplates() error { 811 walker := func(path string, fi hugofs.FileMetaInfo) error { 812 if fi.IsDir() { 813 return nil 814 } 815 816 if isDotFile(path) || isBackupFile(path) { 817 return nil 818 } 819 820 name := strings.TrimPrefix(filepath.ToSlash(path), "/") 821 filename := filepath.Base(path) 822 outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats) 823 outputFormat, found := outputFormats.FromFilename(filename) 824 825 if found && outputFormat.IsPlainText { 826 name = textTmplNamePrefix + name 827 } 828 829 if err := t.addTemplateFile(name, fi); err != nil { 830 return err 831 } 832 833 return nil 834 } 835 836 if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil { 837 if !herrors.IsNotExist(err) { 838 return err 839 } 840 return nil 841 } 842 843 return nil 844 } 845 846 func (t *templateHandler) nameIsText(name string) (string, bool) { 847 isText := strings.HasPrefix(name, textTmplNamePrefix) 848 if isText { 849 name = strings.TrimPrefix(name, textTmplNamePrefix) 850 } 851 return name, isText 852 } 853 854 func (t *templateHandler) noBaseNeeded(name string) bool { 855 if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") { 856 return true 857 } 858 return strings.Contains(name, "_markup/") 859 } 860 861 func (t *templateHandler) extractPartials(templ tpl.Template) error { 862 templs := templates(templ) 863 for _, templ := range templs { 864 if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") { 865 continue 866 } 867 868 ts := newTemplateState(templ, templateInfo{name: templ.Name()}, nil) 869 ts.typ = templatePartial 870 871 t.main.mu.RLock() 872 _, found := t.main.templates[templ.Name()] 873 t.main.mu.RUnlock() 874 875 if !found { 876 t.main.mu.Lock() 877 // This is a template defined inline. 878 _, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts)) 879 if err != nil { 880 t.main.mu.Unlock() 881 return err 882 } 883 t.main.templates[templ.Name()] = ts 884 t.main.mu.Unlock() 885 886 } 887 } 888 889 return nil 890 } 891 892 func (t *templateHandler) postTransform() error { 893 defineCheckedHTML := false 894 defineCheckedText := false 895 896 for _, v := range t.main.templates { 897 if v.typ == templateShortcode { 898 t.addShortcodeVariant(v) 899 } 900 901 if defineCheckedHTML && defineCheckedText { 902 continue 903 } 904 905 isText := isText(v.Template) 906 if isText { 907 if defineCheckedText { 908 continue 909 } 910 defineCheckedText = true 911 } else { 912 if defineCheckedHTML { 913 continue 914 } 915 defineCheckedHTML = true 916 } 917 918 if err := t.extractPartials(v.Template); err != nil { 919 return err 920 } 921 } 922 923 for name, source := range t.transformNotFound { 924 lookup := t.main.newTemplateLookup(source) 925 templ := lookup(name) 926 if templ != nil { 927 _, err := applyTemplateTransformers(templ, lookup) 928 if err != nil { 929 return err 930 } 931 } 932 } 933 934 for _, v := range t.shortcodes { 935 sort.Slice(v.variants, func(i, j int) bool { 936 v1, v2 := v.variants[i], v.variants[j] 937 name1, name2 := v1.ts.Name(), v2.ts.Name() 938 isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html") 939 940 // There will be a weighted selection later, but make 941 // sure these are sorted to get a stable selection for 942 // output formats missing specific templates. 943 // Prefer HTML. 944 if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) { 945 return isHTMl1 946 } 947 948 return name1 < name2 949 }) 950 } 951 952 return nil 953 } 954 955 type templateNamespace struct { 956 prototypeText *texttemplate.Template 957 prototypeHTML *htmltemplate.Template 958 prototypeTextClone *texttemplate.Template 959 prototypeHTMLClone *htmltemplate.Template 960 961 *templateStateMap 962 } 963 964 func (t templateNamespace) Clone() *templateNamespace { 965 t.mu.Lock() 966 967 t.templateStateMap = &templateStateMap{ 968 templates: make(map[string]*templateState), 969 } 970 971 t.prototypeText = texttemplate.Must(t.prototypeText.Clone()) 972 t.prototypeHTML = htmltemplate.Must(t.prototypeHTML.Clone()) 973 974 t.mu.Unlock() 975 return &t 976 } 977 978 func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) { 979 t.mu.RLock() 980 981 templ, found := t.templates[name] 982 if !found { 983 t.mu.RUnlock() 984 return nil, false 985 } 986 t.mu.RUnlock() 987 return templ, found 988 } 989 990 func (t *templateNamespace) createPrototypes() error { 991 t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone()) 992 t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone()) 993 994 return nil 995 } 996 997 func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState { 998 return func(name string) *templateState { 999 if templ, found := t.templates[name]; found { 1000 if templ.isText() != in.isText() { 1001 return nil 1002 } 1003 return templ 1004 } 1005 if templ, found := findTemplateIn(name, in); found { 1006 return newTemplateState(templ, templateInfo{name: templ.Name()}, nil) 1007 } 1008 return nil 1009 } 1010 } 1011 1012 func (t *templateNamespace) parse(info templateInfo) (*templateState, error) { 1013 t.mu.Lock() 1014 defer t.mu.Unlock() 1015 1016 if info.isText { 1017 prototype := t.prototypeText 1018 1019 templ, err := prototype.New(info.name).Parse(info.template) 1020 if err != nil { 1021 return nil, err 1022 } 1023 1024 ts := newTemplateState(templ, info, nil) 1025 1026 t.templates[info.name] = ts 1027 1028 return ts, nil 1029 } 1030 1031 prototype := t.prototypeHTML 1032 1033 templ, err := prototype.New(info.name).Parse(info.template) 1034 if err != nil { 1035 return nil, err 1036 } 1037 1038 ts := newTemplateState(templ, info, nil) 1039 1040 t.templates[info.name] = ts 1041 1042 return ts, nil 1043 } 1044 1045 var _ tpl.IsInternalTemplateProvider = (*templateState)(nil) 1046 1047 type templateState struct { 1048 tpl.Template 1049 1050 typ templateType 1051 parseInfo tpl.ParseInfo 1052 id identity.Identity 1053 1054 info templateInfo 1055 baseInfo templateInfo // Set when a base template is used. 1056 } 1057 1058 func (t *templateState) IsInternalTemplate() bool { 1059 return t.info.isEmbedded 1060 } 1061 1062 func (t *templateState) GetIdentity() identity.Identity { 1063 return t.id 1064 } 1065 1066 func (t *templateState) ParseInfo() tpl.ParseInfo { 1067 return t.parseInfo 1068 } 1069 1070 func (t *templateState) isText() bool { 1071 return isText(t.Template) 1072 } 1073 1074 func (t *templateState) String() string { 1075 return t.Name() 1076 } 1077 1078 func isText(templ tpl.Template) bool { 1079 _, isText := templ.(*texttemplate.Template) 1080 return isText 1081 } 1082 1083 type templateStateMap struct { 1084 mu sync.RWMutex //nolint 1085 templates map[string]*templateState 1086 } 1087 1088 type textTemplateWrapperWithLock struct { 1089 *sync.RWMutex 1090 *texttemplate.Template 1091 } 1092 1093 func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) { 1094 t.RLock() 1095 templ := t.Template.Lookup(name) 1096 t.RUnlock() 1097 if templ == nil { 1098 return nil, false 1099 } 1100 return &textTemplateWrapperWithLock{ 1101 RWMutex: t.RWMutex, 1102 Template: templ, 1103 }, true 1104 } 1105 1106 func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { 1107 panic("not supported") 1108 } 1109 1110 func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template { 1111 panic("not supported") 1112 } 1113 1114 func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) { 1115 t.Lock() 1116 defer t.Unlock() 1117 return t.Template.New(name).Parse(tpl) 1118 } 1119 1120 func isBackupFile(path string) bool { 1121 return path[len(path)-1] == '~' 1122 } 1123 1124 func isBaseTemplatePath(path string) bool { 1125 return strings.Contains(filepath.Base(path), baseFileBase) 1126 } 1127 1128 func isDotFile(path string) bool { 1129 return filepath.Base(path)[0] == '.' 1130 } 1131 1132 func removeLeadingBOM(s string) string { 1133 const bom = '\ufeff' 1134 1135 for i, r := range s { 1136 if i == 0 && r != bom { 1137 return s 1138 } 1139 if i > 0 { 1140 return s[i:] 1141 } 1142 } 1143 1144 return s 1145 } 1146 1147 // resolves _internal/shortcodes/param.html => param.html etc. 1148 func templateBaseName(typ templateType, name string) string { 1149 name = strings.TrimPrefix(name, internalPathPrefix) 1150 switch typ { 1151 case templateShortcode: 1152 return strings.TrimPrefix(name, shortcodesPathPrefix) 1153 default: 1154 panic("not implemented") 1155 } 1156 } 1157 1158 func unwrap(templ tpl.Template) tpl.Template { 1159 if ts, ok := templ.(*templateState); ok { 1160 return ts.Template 1161 } 1162 return templ 1163 } 1164 1165 func templates(in tpl.Template) []tpl.Template { 1166 var templs []tpl.Template 1167 in = unwrap(in) 1168 if textt, ok := in.(*texttemplate.Template); ok { 1169 for _, t := range textt.Templates() { 1170 templs = append(templs, t) 1171 } 1172 } 1173 1174 if htmlt, ok := in.(*htmltemplate.Template); ok { 1175 for _, t := range htmlt.Templates() { 1176 templs = append(templs, t) 1177 } 1178 } 1179 1180 return templs 1181 }