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