summaryrefslogtreecommitdiffstats
path: root/hugolib/page__per_output.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-01-02 12:33:26 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-03-23 18:51:22 +0100
commit597e418cb02883418f2cebb41400e8e61413f651 (patch)
tree177ad9c540b2583b6dab138c9f0490d28989c7f7 /hugolib/page__per_output.go
parent44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff)
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct. This is all a preparation step for issue #5074, "pages from other data sources". But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes. Most notable changes: * The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday. This means that any markdown will partake in the global ToC and footnote context etc. * The Custom Output formats are now "fully virtualized". This removes many of the current limitations. * The taxonomy list type now has a reference to the `Page` object. This improves the taxonomy template `.Title` situation and make common template constructs much simpler. See #5074 Fixes #5763 Fixes #5758 Fixes #5090 Fixes #5204 Fixes #4695 Fixes #5607 Fixes #5707 Fixes #5719 Fixes #3113 Fixes #5706 Fixes #5767 Fixes #5723 Fixes #5769 Fixes #5770 Fixes #5771 Fixes #5759 Fixes #5776 Fixes #5777 Fixes #5778
Diffstat (limited to 'hugolib/page__per_output.go')
-rw-r--r--hugolib/page__per_output.go445
1 files changed, 445 insertions, 0 deletions
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
new file mode 100644
index 000000000..05b35cc87
--- /dev/null
+++ b/hugolib/page__per_output.go
@@ -0,0 +1,445 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugolib
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html/template"
+ "strings"
+ "sync"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/lazy"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/tpl"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ nopTargetPath = targetPathsHolder{}
+ nopPagePerOutput = struct {
+ resource.ResourceLinksProvider
+ page.ContentProvider
+ page.PageRenderProvider
+ page.PaginatorProvider
+ page.TableOfContentsProvider
+ page.AlternativeOutputFormatsProvider
+
+ targetPather
+ }{
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ nopTargetPath,
+ }
+)
+
+func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
+
+ parent := p.init
+
+ return func(f output.Format) (*pageContentOutput, error) {
+ cp := &pageContentOutput{
+ p: p,
+ f: f,
+ }
+
+ initContent := func() error {
+ var err error
+ var hasVariants bool
+
+ cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
+ if err != nil {
+ return err
+ }
+
+ if p.render && !hasVariants {
+ // We can reuse this for the other output formats
+ cp.enableReuse()
+ }
+
+ cp.workContent = p.contentToRender(cp.contentPlaceholders)
+
+ isHTML := cp.p.m.markup == "html"
+
+ if p.renderable {
+ if !isHTML {
+ cp.workContent = cp.renderContent(p, cp.workContent)
+ tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
+ cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
+ cp.workContent = tmpContent
+ }
+
+ if cp.placeholdersEnabled {
+ // ToC was accessed via .Page.TableOfContents in the shortcode,
+ // at a time when the ToC wasn't ready.
+ cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
+ }
+
+ if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
+ // There are one or more replacement tokens to be replaced.
+ cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
+ if err != nil {
+ return err
+ }
+ }
+
+ if cp.p.source.hasSummaryDivider {
+ if isHTML {
+ src := p.source.parsed.Input()
+
+ // Use the summary sections as they are provided by the user.
+ if p.source.posSummaryEnd != -1 {
+ cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
+ }
+
+ if cp.p.source.posBodyStart != -1 {
+ cp.workContent = src[cp.p.source.posBodyStart:]
+ }
+
+ } else {
+ summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
+ if err != nil {
+ cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
+ } else {
+ cp.workContent = content
+ cp.summary = helpers.BytesToHTML(summary)
+ }
+ }
+ }
+ }
+
+ cp.content = helpers.BytesToHTML(cp.workContent)
+
+ if !p.renderable {
+ err := cp.addSelfTemplate()
+ return err
+ }
+
+ return nil
+
+ }
+
+ // Recursive loops can only happen in content files with template code (shortcodes etc.)
+ // Avoid creating new goroutines if we don't have to.
+ needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
+
+ if needTimeout {
+ cp.initMain = parent.BranchdWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+ return nil, initContent()
+ })
+ } else {
+ cp.initMain = parent.Branch(func() (interface{}, error) {
+ return nil, initContent()
+ })
+ }
+
+ cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
+ cp.plain = helpers.StripHTML(string(cp.content))
+ cp.plainWords = strings.Fields(cp.plain)
+ cp.setWordCounts(p.m.isCJKLanguage)
+
+ if err := cp.setAutoSummary(); err != nil {
+ return err, nil
+ }
+
+ return nil, nil
+ })
+
+ return cp, nil
+
+ }
+
+}
+
+// pageContentOutput represents the Page content for a given output format.
+type pageContentOutput struct {
+ f output.Format
+
+ // If we can safely reuse this for other output formats.
+ reuse bool
+ reuseInit sync.Once
+
+ p *pageState
+
+ // Lazy load dependencies
+ initMain *lazy.Init
+ initPlain *lazy.Init
+
+ placeholdersEnabled bool
+ placeholdersEnabledInit sync.Once
+
+ // Content state
+
+ workContent []byte
+
+ // Temporary storage of placeholders mapped to their content.
+ // These are shortcodes etc. Some of these will need to be replaced
+ // after any markup is rendered, so they share a common prefix.
+ contentPlaceholders map[string]string
+
+ // Content sections
+ content template.HTML
+ summary template.HTML
+ tableOfContents template.HTML
+
+ truncated bool
+
+ plainWords []string
+ plain string
+ fuzzyWordCount int
+ wordCount int
+ readingTime int
+}
+
+func (p *pageContentOutput) Content() (interface{}, error) {
+ p.p.s.initInit(p.initMain, p.p)
+ return p.content, nil
+}
+
+func (p *pageContentOutput) FuzzyWordCount() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.fuzzyWordCount
+}
+
+func (p *pageContentOutput) Len() int {
+ p.p.s.initInit(p.initMain, p.p)
+ return len(p.content)
+}
+
+func (p *pageContentOutput) Plain() string {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.plain
+}
+
+func (p *pageContentOutput) PlainWords() []string {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.plainWords
+}
+
+func (p *pageContentOutput) ReadingTime() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.readingTime
+}
+
+func (p *pageContentOutput) Summary() template.HTML {
+ p.p.s.initInit(p.initMain, p.p)
+ if !p.p.source.hasSummaryDivider {
+ p.p.s.initInit(p.initPlain, p.p)
+ }
+ return p.summary
+}
+
+func (p *pageContentOutput) TableOfContents() template.HTML {
+ p.p.s.initInit(p.initMain, p.p)
+ return p.tableOfContents
+}
+
+func (p *pageContentOutput) Truncated() bool {
+ if p.p.truncated {
+ return true
+ }
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.truncated
+}
+
+func (p *pageContentOutput) WordCount() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.wordCount
+}
+
+func (p *pageContentOutput) setAutoSummary() error {
+ if p.p.source.hasSummaryDivider {
+ return nil
+ }
+
+ var summary string
+ var truncated bool
+
+ if p.p.m.isCJKLanguage {
+ summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
+ } else {
+ summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
+ }
+ p.summary = template.HTML(summary)
+
+ p.truncated = truncated
+
+ return nil
+
+}
+
+func (cp *pageContentOutput) renderContent(p page.Page, content []byte) []byte {
+ return cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
+ Content: content, RenderTOC: true, PageFmt: cp.p.m.markup,
+ Cfg: p.Language(),
+ DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
+ Config: cp.p.getRenderingConfig()})
+}
+
+func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
+ if isCJKLanguage {
+ p.wordCount = 0
+ for _, word := range p.plainWords {
+ runeCount := utf8.RuneCountInString(word)
+ if len(word) == runeCount {
+ p.wordCount++
+ } else {
+ p.wordCount += runeCount
+ }
+ }
+ } else {
+ p.wordCount = helpers.TotalWords(p.plain)
+ }
+
+ // TODO(bep) is set in a test. Fix that.
+ if p.fuzzyWordCount == 0 {
+ p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
+ }
+
+ if isCJKLanguage {
+ p.readingTime = (p.wordCount + 500) / 501
+ } else {
+ p.readingTime = (p.wordCount + 212) / 213
+ }
+}
+
+func (p *pageContentOutput) addSelfTemplate() error {
+ self := p.p.selfLayoutForOutput(p.f)
+ err := p.p.s.TemplateHandler().AddLateTemplate(self, string(p.content))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// A callback to signal that we have inserted a placeholder into the rendered
+// content. This avoids doing extra replacement work.
+func (p *pageContentOutput) enablePlaceholders() {
+ p.placeholdersEnabledInit.Do(func() {
+ p.placeholdersEnabled = true
+ })
+}
+
+func (p *pageContentOutput) enableReuse() {
+ p.reuseInit.Do(func() {
+ p.reuse = true
+ })
+}
+
+// these will be shifted out when rendering a given output format.
+type pagePerOutputProviders interface {
+ targetPather
+ page.ContentProvider
+ page.PaginatorProvider
+ page.TableOfContentsProvider
+ resource.ResourceLinksProvider
+}
+
+type targetPather interface {
+ targetPaths() page.TargetPaths
+}
+
+type targetPathsHolder struct {
+ paths page.TargetPaths
+ page.OutputFormat
+}
+
+func (t targetPathsHolder) targetPaths() page.TargetPaths {
+ return t.paths
+}
+
+func executeToString(templ tpl.Template, data interface{}) (string, error) {
+ b := bp.GetBuffer()
+ defer bp.PutBuffer(b)
+ if err := templ.Execute(b, data); err != nil {
+ return "", err
+ }
+ return b.String(), nil
+
+}
+
+func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("summary split failed: %s", r)
+ }
+ }()
+
+ startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
+
+ if startDivider == -1 {
+ return
+ }
+
+ startTag := "p"
+ switch markup {
+ case "asciidoc":
+ startTag = "div"
+
+ }
+
+ // Walk back and forward to the surrounding tags.
+ start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
+ end := bytes.Index(c[startDivider:], []byte("</"+startTag))
+
+ if start == -1 {
+ start = startDivider
+ } else {
+ start = startDivider - (startDivider - start)
+ }
+
+ if end == -1 {
+ end = startDivider + len(internalSummaryDividerBase)
+ } else {
+ end = startDivider + end + len(startTag) + 3
+ }
+
+ var addDiv bool
+
+ switch markup {
+ case "rst":
+ addDiv = true
+ }
+
+ withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
+
+ if len(withoutDivider) > 0 {
+ summary = bytes.TrimSpace(withoutDivider[:start])
+ }
+
+ if addDiv {
+ // For the rst
+ summary = append(append([]byte(nil), summary...), []byte("</div>")...)
+ }
+
+ if err != nil {
+ return
+ }
+
+ content = bytes.TrimSpace(withoutDivider)
+
+ return
+}