github.com/dominikszabo/hugo-ds-clean@v0.47.1/hugolib/page_output.go (about)

     1  // Copyright 2017 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  	"html/template"
    19  	"os"
    20  	"strings"
    21  	"sync"
    22  
    23  	bp "github.com/gohugoio/hugo/bufferpool"
    24  
    25  	"github.com/gohugoio/hugo/tpl"
    26  
    27  	"github.com/gohugoio/hugo/resource"
    28  
    29  	"github.com/gohugoio/hugo/media"
    30  
    31  	"github.com/gohugoio/hugo/output"
    32  )
    33  
    34  // PageOutput represents one of potentially many output formats of a given
    35  // Page.
    36  type PageOutput struct {
    37  	*Page
    38  
    39  	// Pagination
    40  	paginator     *Pager
    41  	paginatorInit sync.Once
    42  
    43  	// Page output specific resources
    44  	resources     resource.Resources
    45  	resourcesInit sync.Once
    46  
    47  	// Keep this to create URL/path variations, i.e. paginators.
    48  	targetPathDescriptor targetPathDescriptor
    49  
    50  	outputFormat output.Format
    51  }
    52  
    53  func (p *PageOutput) targetPath(addends ...string) (string, error) {
    54  	tp, err := p.createTargetPath(p.outputFormat, false, addends...)
    55  	if err != nil {
    56  		return "", err
    57  	}
    58  	return tp, nil
    59  }
    60  
    61  func newPageOutput(p *Page, createCopy, initContent bool, f output.Format) (*PageOutput, error) {
    62  	// TODO(bep) This is only needed for tests and we should get rid of it.
    63  	if p.targetPathDescriptorPrototype == nil {
    64  		if err := p.initPaths(); err != nil {
    65  			return nil, err
    66  		}
    67  	}
    68  
    69  	if createCopy {
    70  		p = p.copy(initContent)
    71  	}
    72  
    73  	td, err := p.createTargetPathDescriptor(f)
    74  
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return &PageOutput{
    80  		Page:                 p,
    81  		outputFormat:         f,
    82  		targetPathDescriptor: td,
    83  	}, nil
    84  }
    85  
    86  // copy creates a copy of this PageOutput with the lazy sync.Once vars reset
    87  // so they will be evaluated again, for word count calculations etc.
    88  func (p *PageOutput) copyWithFormat(f output.Format, initContent bool) (*PageOutput, error) {
    89  	c, err := newPageOutput(p.Page, true, initContent, f)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	c.paginator = p.paginator
    94  	return c, nil
    95  }
    96  
    97  func (p *PageOutput) copy() (*PageOutput, error) {
    98  	return p.copyWithFormat(p.outputFormat, false)
    99  }
   100  
   101  func (p *PageOutput) layouts(layouts ...string) ([]string, error) {
   102  	if len(layouts) == 0 && p.selfLayout != "" {
   103  		return []string{p.selfLayout}, nil
   104  	}
   105  
   106  	layoutDescriptor := p.layoutDescriptor
   107  
   108  	if len(layouts) > 0 {
   109  		layoutDescriptor.Layout = layouts[0]
   110  		layoutDescriptor.LayoutOverride = true
   111  	}
   112  
   113  	return p.s.layoutHandler.For(
   114  		layoutDescriptor,
   115  		p.outputFormat)
   116  }
   117  
   118  func (p *PageOutput) Render(layout ...string) template.HTML {
   119  	l, err := p.layouts(layout...)
   120  	if err != nil {
   121  		p.s.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle())
   122  		return ""
   123  	}
   124  
   125  	for _, layout := range l {
   126  		templ, found := p.s.Tmpl.Lookup(layout)
   127  		if !found {
   128  			// This is legacy from when we had only one output format and
   129  			// HTML templates only. Some have references to layouts without suffix.
   130  			// We default to good old HTML.
   131  			templ, found = p.s.Tmpl.Lookup(layout + ".html")
   132  		}
   133  		if templ != nil {
   134  			res, err := executeToString(templ, p)
   135  			if err != nil {
   136  				p.s.DistinctErrorLog.Printf("in .Render: Failed to execute template %q: %s", layout, err)
   137  				return template.HTML("")
   138  			}
   139  			return template.HTML(res)
   140  		}
   141  	}
   142  
   143  	return ""
   144  
   145  }
   146  
   147  func executeToString(templ tpl.Template, data interface{}) (string, error) {
   148  	b := bp.GetBuffer()
   149  	defer bp.PutBuffer(b)
   150  	if err := templ.Execute(b, data); err != nil {
   151  		return "", err
   152  	}
   153  	return b.String(), nil
   154  
   155  }
   156  
   157  func (p *Page) Render(layout ...string) template.HTML {
   158  	if p.mainPageOutput == nil {
   159  		panic(fmt.Sprintf("programming error: no mainPageOutput for %q", p.Path()))
   160  	}
   161  	return p.mainPageOutput.Render(layout...)
   162  }
   163  
   164  // OutputFormats holds a list of the relevant output formats for a given resource.
   165  type OutputFormats []*OutputFormat
   166  
   167  // OutputFormat links to a representation of a resource.
   168  type OutputFormat struct {
   169  	// Rel constains a value that can be used to construct a rel link.
   170  	// This is value is fetched from the output format definition.
   171  	// Note that for pages with only one output format,
   172  	// this method will always return "canonical".
   173  	// As an example, the AMP output format will, by default, return "amphtml".
   174  	//
   175  	// See:
   176  	// https://www.ampproject.org/docs/guides/deploy/discovery
   177  	//
   178  	// Most other output formats will have "alternate" as value for this.
   179  	Rel string
   180  
   181  	// It may be tempting to export this, but let us hold on to that horse for a while.
   182  	f output.Format
   183  
   184  	p *Page
   185  }
   186  
   187  // Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc.
   188  func (o OutputFormat) Name() string {
   189  	return o.f.Name
   190  }
   191  
   192  // MediaType returns this OutputFormat's MediaType (MIME type).
   193  func (o OutputFormat) MediaType() media.Type {
   194  	return o.f.MediaType
   195  }
   196  
   197  // OutputFormats gives the output formats for this Page.
   198  func (p *Page) OutputFormats() OutputFormats {
   199  	var o OutputFormats
   200  	for _, f := range p.outputFormats {
   201  		o = append(o, newOutputFormat(p, f))
   202  	}
   203  	return o
   204  }
   205  
   206  func newOutputFormat(p *Page, f output.Format) *OutputFormat {
   207  	rel := f.Rel
   208  	isCanonical := len(p.outputFormats) == 1
   209  	if isCanonical {
   210  		rel = "canonical"
   211  	}
   212  	return &OutputFormat{Rel: rel, f: f, p: p}
   213  }
   214  
   215  // AlternativeOutputFormats gives the alternative output formats for this PageOutput.
   216  // Note that we use the term "alternative" and not "alternate" here, as it
   217  // does not necessarily replace the other format, it is an alternative representation.
   218  func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) {
   219  	var o OutputFormats
   220  	for _, of := range p.OutputFormats() {
   221  		if of.f.NotAlternative || of.f.Name == p.outputFormat.Name {
   222  			continue
   223  		}
   224  		o = append(o, of)
   225  	}
   226  	return o, nil
   227  }
   228  
   229  // deleteResource removes the resource from this PageOutput and the Page. They will
   230  // always be of the same length, but may contain different elements.
   231  func (p *PageOutput) deleteResource(i int) {
   232  	p.resources = append(p.resources[:i], p.resources[i+1:]...)
   233  	p.Page.Resources = append(p.Page.Resources[:i], p.Page.Resources[i+1:]...)
   234  
   235  }
   236  
   237  func (p *PageOutput) Resources() resource.Resources {
   238  	p.resourcesInit.Do(func() {
   239  		// If the current out shares the same path as the main page output, we reuse
   240  		// the resource set. For the "amp" use case, we need to clone them with new
   241  		// base folder.
   242  		ff := p.outputFormats[0]
   243  		if p.outputFormat.Path == ff.Path {
   244  			p.resources = p.Page.Resources
   245  			return
   246  		}
   247  
   248  		// Clone it with new base.
   249  		resources := make(resource.Resources, len(p.Page.Resources))
   250  
   251  		for i, r := range p.Page.Resources {
   252  			if c, ok := r.(resource.Cloner); ok {
   253  				// Clone the same resource with a new target.
   254  				resources[i] = c.WithNewBase(p.outputFormat.Path)
   255  			} else {
   256  				resources[i] = r
   257  			}
   258  		}
   259  
   260  		p.resources = resources
   261  	})
   262  
   263  	return p.resources
   264  }
   265  
   266  func (p *PageOutput) renderResources() error {
   267  
   268  	for i, r := range p.Resources() {
   269  		src, ok := r.(resource.Source)
   270  		if !ok {
   271  			// Pages gets rendered with the owning page.
   272  			continue
   273  		}
   274  
   275  		if err := src.Publish(); err != nil {
   276  			if os.IsNotExist(err) {
   277  				// The resource has been deleted from the file system.
   278  				// This should be extremely rare, but can happen on live reload in server
   279  				// mode when the same resource is member of different page bundles.
   280  				p.deleteResource(i)
   281  			} else {
   282  				p.s.Log.ERROR.Printf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
   283  			}
   284  		} else {
   285  			p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files)
   286  		}
   287  	}
   288  	return nil
   289  }
   290  
   291  // AlternativeOutputFormats is only available on the top level rendering
   292  // entry point, and not inside range loops on the Page collections.
   293  // This method is just here to inform users of that restriction.
   294  func (p *Page) AlternativeOutputFormats() (OutputFormats, error) {
   295  	return nil, fmt.Errorf("AlternativeOutputFormats only available from the top level template context for page %q", p.Path())
   296  }
   297  
   298  // Get gets a OutputFormat given its name, i.e. json, html etc.
   299  // It returns nil if not found.
   300  func (o OutputFormats) Get(name string) *OutputFormat {
   301  	for _, f := range o {
   302  		if strings.EqualFold(f.f.Name, name) {
   303  			return f
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  // Permalink returns the absolute permalink to this output format.
   310  func (o *OutputFormat) Permalink() string {
   311  	rel := o.p.createRelativePermalinkForOutputFormat(o.f)
   312  	perm, _ := o.p.s.permalinkForOutputFormat(rel, o.f)
   313  	return perm
   314  }
   315  
   316  // RelPermalink returns the relative permalink to this output format.
   317  func (o *OutputFormat) RelPermalink() string {
   318  	rel := o.p.createRelativePermalinkForOutputFormat(o.f)
   319  	return o.p.s.PathSpec.PrependBasePath(rel)
   320  }