// Copyright 2024 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 page import ( "context" "html/template" "regexp" "strings" "unicode" "unicode/utf8" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/tpl" ) type Content interface { Content(context.Context) (template.HTML, error) ContentWithoutSummary(context.Context) (template.HTML, error) Summary(context.Context) (Summary, error) Plain(context.Context) string PlainWords(context.Context) []string WordCount(context.Context) int FuzzyWordCount(context.Context) int ReadingTime(context.Context) int Len(context.Context) int } type Markup interface { Render(context.Context) (Content, error) RenderString(ctx context.Context, args ...any) (template.HTML, error) RenderShortcodes(context.Context) (template.HTML, error) Fragments(context.Context) *tableofcontents.Fragments } var _ types.PrintableValueProvider = Summary{} const ( SummaryTypeAuto = "auto" SummaryTypeManual = "manual" SummaryTypeFrontMatter = "frontmatter" ) type Summary struct { Text template.HTML Type string // "auto", "manual" or "frontmatter" Truncated bool } func (s Summary) IsZero() bool { return s.Text == "" } func (s Summary) PrintableValue() any { return s.Text } var _ types.PrintableValueProvider = (*Summary)(nil) type HtmlSummary struct { source string SummaryLowHigh types.LowHigh[string] SummaryEndTag types.LowHigh[string] WrapperStart types.LowHigh[string] WrapperEnd types.LowHigh[string] Divider types.LowHigh[string] } func (s HtmlSummary) wrap(ss string) string { if s.WrapperStart.IsZero() { return ss } return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss + s.source[s.WrapperEnd.Low:s.WrapperEnd.High] } func (s HtmlSummary) wrapLeft(ss string) string { if s.WrapperStart.IsZero() { return ss } return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss } func (s HtmlSummary) Value(l types.LowHigh[string]) string { return s.source[l.Low:l.High] } func (s HtmlSummary) trimSpace(ss string) string { return strings.TrimSpace(ss) } func (s HtmlSummary) Content() string { if s.Divider.IsZero() { return s.source } ss := s.source[:s.Divider.Low] ss += s.source[s.Divider.High:] return s.trimSpace(ss) } func (s HtmlSummary) Summary() string { if s.Divider.IsZero() { return s.trimSpace(s.wrap(s.Value(s.SummaryLowHigh))) } ss := s.source[s.SummaryLowHigh.Low:s.Divider.Low] if s.SummaryLowHigh.High > s.Divider.High { ss += s.source[s.Divider.High:s.SummaryLowHigh.High] } if !s.SummaryEndTag.IsZero() { ss += s.Value(s.SummaryEndTag) } return s.trimSpace(s.wrap(ss)) } func (s HtmlSummary) ContentWithoutSummary() string { if s.Divider.IsZero() { if s.SummaryLowHigh.Low == s.WrapperStart.High && s.SummaryLowHigh.High == s.WrapperEnd.Low { return "" } return s.trimSpace(s.wrapLeft(s.source[s.SummaryLowHigh.High:])) } if s.SummaryEndTag.IsZero() { return s.trimSpace(s.wrapLeft(s.source[s.Divider.High:])) } return s.trimSpace(s.wrapLeft(s.source[s.SummaryEndTag.High:])) } func (s HtmlSummary) Truncated() bool { return s.SummaryLowHigh.High < len(s.source) } func (s *HtmlSummary) resolveParagraphTagAndSetWrapper(mt media.Type) tagReStartEnd { ptag := startEndP switch mt.SubType { case media.DefaultContentTypes.AsciiDoc.SubType: ptag = startEndDiv case media.DefaultContentTypes.ReStructuredText.SubType: const markerStart = "
]?>|
]*?>$`), endEndOfString: regexp.MustCompile(`
$`), tagName: "p", } ) type tagReStartEnd struct { startEndOfString *regexp.Regexp endEndOfString *regexp.Regexp tagName string } func expandSummaryDivider(s string, re tagReStartEnd, divider types.LowHigh[string]) (types.LowHigh[string], types.LowHigh[string]) { var endMarkup types.LowHigh[string] if divider.IsZero() { return divider, endMarkup } lo, hi := divider.Low, divider.High var preserveEndMarkup bool // Find the start of the paragraph. for i := lo - 1; i >= 0; i-- { if s[i] == '>' { if match := re.startEndOfString.FindString(s[:i+1]); match != "" { lo = i - len(match) + 1 break } if match := pOrDiv.FindString(s[:i+1]); match != "" { i -= len(match) - 1 continue } } r, _ := utf8.DecodeRuneInString(s[i:]) if !unicode.IsSpace(r) { preserveEndMarkup = true break } } divider.Low = lo // Now walk forward to the end of the paragraph. for ; hi < len(s); hi++ { if s[hi] != '>' { continue } if match := re.endEndOfString.FindString(s[:hi+1]); match != "" { hi++ break } } if preserveEndMarkup { endMarkup.Low = divider.High endMarkup.High = hi } else { divider.High = hi } // Consume trailing newline if any. if divider.High < len(s) && s[divider.High] == '\n' { divider.High++ } return divider, endMarkup }