diff options
Diffstat (limited to 'hugolib/page.go')
-rw-r--r-- | hugolib/page.go | 1029 |
1 files changed, 1029 insertions, 0 deletions
diff --git a/hugolib/page.go b/hugolib/page.go new file mode 100644 index 000000000..baf5e7f69 --- /dev/null +++ b/hugolib/page.go @@ -0,0 +1,1029 @@ +// 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" + "fmt" + "html/template" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/mitchellh/mapstructure" + + "github.com/gohugoio/hugo/identity" + + "github.com/gohugoio/hugo/markup/converter" + + "github.com/gohugoio/hugo/tpl" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/bep/gitmap" + + "github.com/gohugoio/hugo/helpers" + + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/parser/metadecoders" + + "github.com/gohugoio/hugo/parser/pageparser" + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/output" + + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/source" + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/common/collections" + "github.com/gohugoio/hugo/common/text" + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/resources" + "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/resource" +) + +var ( + _ page.Page = (*pageState)(nil) + _ collections.Grouper = (*pageState)(nil) + _ collections.Slicer = (*pageState)(nil) +) + +var ( + pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType) + nopPageOutput = &pageOutput{ + pagePerOutputProviders: nopPagePerOutput, + ContentProvider: page.NopPage, + TableOfContentsProvider: page.NopPage, + } +) + +// pageContext provides contextual information about this page, for error +// logging and similar. +type pageContext interface { + posOffset(offset int) text.Position + wrapError(err error) error + getContentConverter() converter.Converter +} + +// wrapErr adds some context to the given error if possible. +func wrapErr(err error, ctx interface{}) error { + if pc, ok := ctx.(pageContext); ok { + return pc.wrapError(err) + } + return err +} + +type pageSiteAdapter struct { + p page.Page + s *Site +} + +func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) { + p, err := pa.s.getPageNew(pa.p, ref) + if p == nil { + // The nil struct has meaning in some situations, mostly to avoid breaking + // existing sites doing $nilpage.IsDescendant($p), which will always return + // false. + p = page.NilPage + } + return p, err +} + +type pageState struct { + // This slice will be of same length as the number of global slice of output + // formats (for all sites). + pageOutputs []*pageOutput + + // This will be shifted out when we start to render a new output format. + *pageOutput + + // Common for all output formats. + *pageCommon +} + +// Eq returns whether the current page equals the given page. +// This is what's invoked when doing `{{ if eq $page $otherPage }}` +func (p *pageState) Eq(other interface{}) bool { + pp, err := unwrapPage(other) + if err != nil { + return false + } + + return p == pp +} + +func (p *pageState) GitInfo() *gitmap.GitInfo { + return p.gitInfo +} + +// GetTerms gets the terms defined on this page in the given taxonomy. +// The pages returned will be ordered according to the front matter. +func (p *pageState) GetTerms(taxonomy string) page.Pages { + if p.treeRef == nil { + return nil + } + + m := p.s.pageMap + + taxonomy = strings.ToLower(taxonomy) + prefix := cleanSectionTreeKey(taxonomy) + self := strings.TrimPrefix(p.treeRef.key, "/") + + var pas page.Pages + + m.taxonomies.WalkQuery(pageMapQuery{Prefix: prefix}, func(s string, n *contentNode) bool { + key := s + self + if tn, found := m.taxonomyEntries.Get(key); found { + vi := tn.(*contentNode).viewInfo + pas = append(pas, pageWithOrdinal{pageState: n.p, ordinal: vi.ordinal}) + } + return false + }) + + page.SortByDefault(pas) + + return pas +} + +func (p *pageState) MarshalJSON() ([]byte, error) { + return page.MarshalPageToJSON(p) +} + +func (p *pageState) getPages() page.Pages { + b := p.bucket + if b == nil { + return nil + } + return b.getPages() +} + +func (p *pageState) getPagesRecursive() page.Pages { + b := p.bucket + if b == nil { + return nil + } + return b.getPagesRecursive() +} + +func (p *pageState) getPagesAndSections() page.Pages { + b := p.bucket + if b == nil { + return nil + } + return b.getPagesAndSections() +} + +func (p *pageState) RegularPagesRecursive() page.Pages { + p.regularPagesRecursiveInit.Do(func() { + var pages page.Pages + switch p.Kind() { + case page.KindSection: + pages = p.getPagesRecursive() + default: + pages = p.RegularPages() + } + p.regularPagesRecursive = pages + }) + return p.regularPagesRecursive +} + +func (p *pageState) PagesRecursive() page.Pages { + return nil +} + +func (p *pageState) RegularPages() page.Pages { + p.regularPagesInit.Do(func() { + var pages page.Pages + + switch p.Kind() { + case page.KindPage: + case page.KindSection, page.KindHome, page.KindTaxonomyTerm: + pages = p.getPages() + case page.KindTaxonomy: + all := p.Pages() + for _, p := range all { + if p.IsPage() { + pages = append(pages, p) + } + } + default: + pages = p.s.RegularPages() + } + + p.regularPages = pages + + }) + + return p.regularPages +} + +func (p *pageState) Pages() page.Pages { + p.pagesInit.Do(func() { + var pages page.Pages + + switch p.Kind() { + case page.KindPage: + case page.KindSection, page.KindHome: + pages = p.getPagesAndSections() + case page.KindTaxonomy: + pages = p.bucket.getTaxonomyEntries() + case page.KindTaxonomyTerm: + pages = p.bucket.getTaxonomies() + default: + pages = p.s.Pages() + } + + p.pages = pages + }) + + return p.pages +} + +// RawContent returns the un-rendered source content without +// any leading front matter. +func (p *pageState) RawContent() string { + if p.source.parsed == nil { + return "" + } + start := p.source.posMainContent + if start == -1 { + start = 0 + } + return string(p.source.parsed.Input()[start:]) +} + +func (p *pageState) sortResources() { + sort.SliceStable(p.resources, func(i, j int) bool { + ri, rj := p.resources[i], p.resources[j] + if ri.ResourceType() < rj.ResourceType() { + return true + } + + p1, ok1 := ri.(page.Page) + p2, ok2 := rj.(page.Page) + + if ok1 != ok2 { + return ok2 + } + + if ok1 { + return page.DefaultPageSort(p1, p2) + } + + // Make sure not to use RelPermalink or any of the other methods that + // trigger lazy publishing. + return ri.Name() < rj.Name() + }) +} + +func (p *pageState) Resources() resource.Resources { + p.resourcesInit.Do(func() { + p.sortResources() + if len(p.m.resourcesMetadata) > 0 { + resources.AssignMetadata(p.m.resourcesMetadata, p.resources...) + p.sortResources() + } + }) + return p.resources +} + +func (p *pageState) HasShortcode(name string) bool { + if p.shortcodeState == nil { + return false + } + + return p.shortcodeState.nameSet[name] +} + +func (p *pageState) Site() page.Site { + return p.s.Info +} + +func (p *pageState) String() string { + if sourceRef := p.sourceRef(); sourceRef != "" { + return fmt.Sprintf("Page(%s)", sourceRef) + } + return fmt.Sprintf("Page(%q)", p.Title()) +} + +// IsTranslated returns whether this content file is translated to +// other language(s). +func (p *pageState) IsTranslated() bool { + p.s.h.init.translations.Do() + return len(p.translations) > 0 +} + +// TranslationKey returns the key used to map language translations of this page. +// It will use the translationKey set in front matter if set, or the content path and +// filename (excluding any language code and extension), e.g. "about/index". +// The Page Kind is always prepended. +func (p *pageState) TranslationKey() string { + p.translationKeyInit.Do(func() { + if p.m.translationKey != "" { + p.translationKey = p.Kind() + "/" + p.m.translationKey + } else if p.IsPage() && !p.File().IsZero() { + p.translationKey = path.Join(p.Kind(), filepath.ToSlash(p.File().Dir()), p.File().TranslationBaseName()) + } else if p.IsNode() { + p.translationKey = path.Join(p.Kind(), p.SectionsPath()) + } + + }) + + return p.translationKey + +} + +// AllTranslations returns all translations, including the current Page. +func (p *pageState) AllTranslations() page.Pages { + p.s.h.init.translations.Do() + return p.allTranslations +} + +// Translations returns the translations excluding the current Page. +func (p *pageState) Translations() page.Pages { + p.s.h.init.translations.Do() + return p.translations +} + +func (ps *pageState) initCommonProviders(pp pagePaths) error { + if ps.IsPage() { + ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext} + ps.posNextPrevSection = &nextPrev{init: ps.s.init.prevNextInSection} + ps.InSectionPositioner = newPagePositionInSection(ps.posNextPrevSection) + ps.Positioner = newPagePosition(ps.posNextPrev) + } + + ps.OutputFormatsProvider = pp + ps.targetPathDescriptor = pp.targetPathDescriptor + ps.RefProvider = newPageRef(ps) + ps.SitesProvider = ps.s.Info + + return nil +} + +func (p *pageState) createRenderHooks(f output.Format) (*hooks.Renderers, error) { + layoutDescriptor := p.getLayoutDescriptor() + layoutDescriptor.RenderingHook = true + layoutDescriptor.LayoutOverride = false + layoutDescriptor.Layout = "" + + var renderers hooks.Renderers + + layoutDescriptor.Kind = "render-link" + templ, templFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f) + if err != nil { + return nil, err + } + if templFound { + renderers.LinkRenderer = hookRenderer{ + templateHandler: p.s.Tmpl(), + Provider: templ.(tpl.Info), + templ: templ, + } + } + + layoutDescriptor.Kind = "render-image" + templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f) + if err != nil { + return nil, err + } + if templFound { + renderers.ImageRenderer = hookRenderer{ + templateHandler: p.s.Tmpl(), + Provider: templ.(tpl.Info), + templ: templ, + } + } + + layoutDescriptor.Kind = "render-heading" + templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f) + if err != nil { + return nil, err + } + if templFound { + renderers.HeadingRenderer = hookRenderer{ + templateHandler: p.s.Tmpl(), + Provider: templ.(tpl.Info), + templ: templ, + } + } + + return &renderers, nil +} + +func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { + p.layoutDescriptorInit.Do(func() { + var section string + sections := p.SectionsEntries() + + switch p.Kind() { + case page.KindSection: + if len(sections) > 0 { + section = sections[0] + } + case page.KindTaxonomyTerm, page.KindTaxonomy: + b := p.getTreeRef().n + section = b.viewInfo.name.singular + default: + } + + p.layoutDescriptor = output.LayoutDescriptor{ + Kind: p.Kind(), + Type: p.Type(), + Lang: p.Language().Lang, + Layout: p.Layout(), + Section: section, + } + }) + + return p.layoutDescriptor + +} + +func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) { + f := p.outputFormat() + + if len(layouts) == 0 { + selfLayout := p.selfLayoutForOutput(f) + if selfLayout != "" { + templ, found := p.s.Tmpl().Lookup(selfLayout) + return templ, found, nil + } + } + + d := p.getLayoutDescriptor() + + if len(layouts) > 0 { + d.Layout = layouts[0] + d.LayoutOverride = true + } + + return p.s.Tmpl().LookupLayout(d, f) +} + +// This is serialized +func (p *pageState) initOutputFormat(isRenderingSite bool, idx int) error { + if err := p.shiftToOutputFormat(isRenderingSite, idx); err != nil { + return err + } + + return nil + +} + +// Must be run after the site section tree etc. is built and ready. +func (p *pageState) initPage() error { + if _, err := p.init.Do(); err != nil { + return err + } + return nil +} + +func (p *pageState) renderResources() (err error) { + p.resourcesPublishInit.Do(func() { + var toBeDeleted []int + + for i, r := range p.Resources() { + + if _, ok := r.(page.Page); ok { + // Pages gets rendered with the owning page but we count them here. + p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) + continue + } + + src, ok := r.(resource.Source) + if !ok { + err = errors.Errorf("Resource %T does not support resource.Source", src) + return + } + + if err := src.Publish(); err != nil { + if os.IsNotExist(err) { + // The resource has been deleted from the file system. + // This should be extremely rare, but can happen on live reload in server + // mode when the same resource is member of different page bundles. + toBeDeleted = append(toBeDeleted, i) + } else { + p.s.Log.ERROR.Printf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err) + } + } else { + p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files) + } + } + + for _, i := range toBeDeleted { + p.deleteResource(i) + } + + }) + + return +} + +func (p *pageState) deleteResource(i int) { + p.resources = append(p.resources[:i], p.resources[i+1:]...) +} + +func (p *pageState) getTargetPaths() page.TargetPaths { + return p.targetPaths() +} + +func (p *pageState) setTranslations(pages page.Pages) { + p.allTranslations = pages + page.SortByLanguage(p.allTranslations) + translations := make(page.Pages, 0) + for _, t := range p.allTranslations { + if !t.Eq(p) { + translations = append(translations, t) + } + } + p.translations = translations +} + +func (p *pageState) AlternativeOutputFormats() page.OutputFormats { + f := p.outputFormat() + var o page.OutputFormats + for _, of := range p.OutputFormats() { + if of.Format.NotAlternative || of.Format.Name == f.Name { + continue + } + + o = append(o, of) + } + return o +} + +type renderStringOpts struct { + Display string + Markup string +} + +var defualtRenderStringOpts = renderStringOpts{ + Display: "inline", + Markup: "", // Will inherit the page's value when not set. +} + +func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) { + if len(args) < 1 || len(args) > 2 { + return "", errors.New("want 1 or 2 arguments") + } + + var s string + opts := defualtRenderStringOpts + sidx := 1 + + if len(args) == 1 { + sidx = 0 + } else { + m, ok := args[0].(map[string]interface{}) + if !ok { + return "", errors.New("first argument must be a map") + } + + if err := mapstructure.WeakDecode(m, &opts); err != nil { + return "", errors.WithMessage(err, "failed to decode options") + } + } + + var err error + s, err = cast.ToStringE(args[sidx]) + if err != nil { + return "", err + } + + if err = p.pageOutput.initRenderHooks(); err != nil { + return "", err + } + + conv := p.getContentConverter() + if opts.Markup != "" && opts.Markup != p.m.markup { + var err error + // TODO(bep) consider cache + conv, err = p.m.newContentConverter(p, opts.Markup, nil) + if err != nil { + return "", p.wrapError(err) + } + } + + c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false) + if err != nil { + return "", p.wrapError(err) + } + + b := c.Bytes() + + if opts.Display == "inline" { + // We may have to rethink this in the future when we get other + // renderers. + b = p.s.ContentSpec.TrimShortHTML(b) + } + + return template.HTML(string(b)), nil +} + +func (p *pageState) addDependency(dep identity.Provider) { + if !p.s.running() || p.pageOutput.cp == nil { + return + } + p.pageOutput.cp.dependencyTracker.Add(dep) +} + +func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) { + p.addDependency(info) + return p.Render(layout...) +} + +func (p *pageState) Render(layout ...string) (template.HTML, error) { + templ, found, err := p.resolveTemplate(layout...) + if err != nil { + return "", p.wrapError(err) + } + + if !found { + return "", nil + } + + p.addDependency(templ.(tpl.Info)) + res, err := executeToString(p.s.Tmpl(), templ, p) + if err != nil { + return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout)) + } + return template.HTML(res), nil + +} + +// wrapError adds some more context to the given error if possible/needed +func (p *pageState) wrapError(err error) error { + if _, ok := err.(*herrors.ErrorWithFileContext); ok { + // Preserve the first file context. + return err + } + var filename string + if !p.File().IsZero() { + filename = p.File().Filename() + } + + err, _ = herrors.WithFileContextForFile( + err, + filename, + filename, + p.s.SourceSpec.Fs.Source, + herrors.SimpleLineMatcher) + + return err +} + +func (p *pageState) getContentConverter() converter.Converter { + var err error + p.m.contentConverterInit.Do(func() { + markup := p.m.markup + if markup == "html" { + // Only used for shortcode inner content. + markup = "markdown" + } + p.m.contentConverter, err = p.m.newContentConverter(p, markup, p.m.renderingConfigOverrides) + + }) + + if err != nil { + p.s.Log.ERROR.Println("Failed to create content converter:", err) + } + return p.m.contentConverter +} + +func (p *pageState) mapContent(bucket *pagesMapBucket, meta *pageMeta) error { + + s := p.shortcodeState + + rn := &pageContentMap{ + items: make([]interface{}, 0, 20), + } + + iter := p.source.parsed.Iterator() + + fail := func(err error, i pageparser.Item) error { + return p.parseError(err, iter.Input(), i.Pos) + } + + // the parser is guaranteed to return items in proper order or fail, so … + // … it's safe to keep some "global" state + var currShortcode shortcode + var ordinal int + var frontMatterSet bool + +Loop: + for { + it := iter.Next() + + switch { + case it.Type == pageparser.TypeIgnore: + case it.IsFrontMatter(): + f := pageparser.FormatFromFrontMatterType(it.Type) + m, err := metadecoders.Default.UnmarshalToMap(it.Val, f) + if err != nil { + if fe, ok := err.(herrors.FileError); ok { + return herrors.ToFileErrorWithOffset(fe, iter.LineNumber()-1) + } else { + return err + } + } + + if err := meta.setMetadata(bucket, p, m); err != nil { + return err + } + + frontMatterSet = true + + next := iter.Peek() + if !next.IsDone() { + p.source.posMainContent = next.Pos + } + + if !p.s.shouldBuild(p) { + // Nothing more to do. + return nil + } + + case it.Type == pageparser.TypeLeadSummaryDivider: + posBody := -1 + f := func(item pageparser.Item) bool { + if posBody == -1 && !item.IsDone() { + posBody = item.Pos + } + + if item.IsNonWhitespace() { + p.truncated = true + + // Done + return false + } + return true + } + iter.PeekWalk(f) + + p.source.posSummaryEnd = it.Pos + p.source.posBodyStart = posBody + p.source.hasSummaryDivider = true + + if meta.markup != "html" { + // The content will be rendered by Blackfriday or similar, + // and we need to track the summary. + rn.AddReplacement(internalSummaryDividerPre, it) + } + + // Handle shortcode + case it.IsLeftShortcodeDelim(): + // let extractShortcode handle left delim (will do so recursively) + iter.Backup() + + currShortcode, err := s.extractShortcode(ordinal, 0, iter) + if err != nil { + return fail(errors.Wrap(err, "failed to extract shortcode"), it) + } + + currShortcode.pos = it.Pos + currShortcode.length = iter.Current().Pos - it.Pos + if currShortcode.placeholder == "" { + currShortcode.placeholder = createShortcodePlaceholder("s", currShortcode.ordinal) + } + + if currShortcode.name != "" { + s.nameSet[currShortcode.name] = true + } + + if currShortcode.params == nil { + var s []string + currShortcode.params = s + } + + currShortcode.placeholder = createShortcodePlaceholder("s", ordinal) + ordinal++ + s.shortcodes = append(s.shortcodes, currShortcode) + + rn.AddShortcode(currShortcode) + + case it.Type == pageparser.TypeEmoji: + if emoji := helpers.Emoji(it.ValStr()); emoji != nil { + rn.AddReplacement(emoji, it) + } else { + rn.AddBytes(it) + } + case it.IsEOF(): + break Loop + case it.IsError(): + err := fail(errors.WithStack(errors.New(it.ValStr())), it) + currShortcode.err = err + return err + + default: + rn.AddBytes(it) + } + } + + if !frontMatterSet { + // Page content without front matter. Assign default front matter from + // cascades etc. + if err := meta.setMetadata(bucket, p, nil); err != nil { + return err + } + } + + p.cmap = rn + + return nil +} + +func (p *pageState) errorf(err error, format string, a ...interface{}) error { + if herrors.UnwrapErrorWithFileContext(err) != nil { + // More isn't always better. + return err + } + args := append([]interface{}{p.Language().Lang, p.pathOrTitle()}, a...) + format = "[%s] page %q: " + format + if err == nil { + errors.Errorf(format, args...) + return fmt.Errorf(format, args...) + } + return errors.Wrapf(err, format, args...) +} + +func (p *pageState) outputFormat() (f output.Format) { + if p.pageOutput == nil { + panic("no pageOutput") + } + return p.pageOutput.f +} + +func (p *pageState) parseError(err error, input []byte, offset int) error { + if herrors.UnwrapFileError(err) != nil { + // Use the most specific location. + return err + } + pos := p.posFromInput(input, offset) + return herrors.NewFileError("md", -1, pos.LineNumber, pos.ColumnNumber, err) + +} + +func (p *pageState) pathOrTitle() string { + if !p.File().IsZero() { + return p.File().Filename() + } + + if p.Path() != "" { + return p.Path() + } + + return p.Title() +} + +func (p *pageState) posFromPage(offset int) text.Position { + return p.posFromInput(p.source.parsed.Input(), offset) +} + +func (p *pageState) posFromInput(input []byte, offset int) text.Position { + lf := []byte("\n") + input = input[:offset] + lineNumber := bytes.Count(input, lf) + 1 + endOfLastLine := bytes.LastIndex(input, lf) + + return text.Position{ + Filename: p.pathOrTitle(), + LineNumber: lineNumber, + ColumnNumber: offset - endOfLastLine, + Offset: offset, + } +} + +func (p *pageState) posOffset(offset int) text.Position { + return p.posFromInput(p.source.parsed.Input(), offset) +} + +// shiftToOutputFormat is serialized. The output format idx refers to the +// full set of output formats for all sites. +func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { + if err := p.initPage(); err != nil { + return err + } + + if len(p.pageOutputs) == 1 { + idx = 0 + } + + p.pageOutput = p.pageOutputs[idx] + if p.pageOutput == nil { + panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx)) + } + + // Reset any built paginator. This will trigger when re-rendering pages in + // server mode. + if isRenderingSite && p.pageOutput.paginator != nil && p.pageOutput.paginator.current != nil { + p.pageOutput.paginator.reset() + } + + if isRenderingSite { + cp := p.pageOutput.cp + if cp == nil { + + // Look for content to reuse. + for i := 0; i < len(p.pageOutputs); i++ { + if i == idx { + continue + } + po := p.pageOutputs[i] + + if po.cp != nil && po.cp.reuse { + cp = po.cp + break + } + } + } + + if cp == nil { + var err error + cp, err = newPageContentOutput(p, p.pageOutput) + if err != nil { + return err + } + } + p.pageOutput.initContentProvider(cp) + p.pageOutput.cp = cp + } + + return nil +} + +// sourceRef returns the reference used by GetPage and ref/relref shortcodes to refer to +// this page. It is prefixed with a "/". +// +// For pages that have a source file, it is returns the path to this file as an +// absolute path rooted in this site's content dir. +// For pages that do not (sections witout content page etc.), it returns the +// virtual path, consistent with where you would add a source file. +func (p *pageState) sourceRef() string { + if !p.File().IsZero() { + sourcePath := p.File().Path() + if sourcePath != "" { + return "/" + filepath.ToSlash(sourcePath) + } + } + + if len(p.SectionsEntries()) > 0 { + // no backing file, return the virtual source path + return "/" + p.SectionsPath() + } + + return "" +} + +func (s *Site) sectionsFromFile(fi source.File) []string { + dirname := fi.Dir() + + dirname = strings.Trim(dirname, helpers.FilePathSeparator) + if dirname == "" { + return nil + } + parts := strings.Split(dirname, helpers.FilePathSeparator) + + if fii, ok := fi.(*fileInfo); ok { + if len(parts) > 0 && fii.FileInfo().Meta().Classifier() == files.ContentClassLeaf { + // my-section/mybundle/index.md => my-section + return parts[:len(parts)-1] + } + } + + return parts +} + +var ( + _ page.Page = (*pageWithOrdinal)(nil) + _ collections.Order = (*pageWithOrdinal)(nil) + _ pageWrapper = (*pageWithOrdinal)(nil) +) + +type pageWithOrdinal struct { + ordinal int + *pageState +} + +func (p pageWithOrdinal) Ordinal() int { + return p.ordinal +} + +func (p pageWithOrdinal) page() page.Page { + return p.pageState +} |