Skip to content

Commit

Permalink
Add Page.Contents with scope support
Browse files Browse the repository at this point in the history
Note that this also adds a new `.ContentWithoutSummary` method, and to do that we had to unify the different summary types:

Both `auto` and `manual` now returns HTML. Before this commit, `auto` would return plain text. This could be considered to be a slightly breaking change, but for the better: Now you can treat the `.Summary` the same without thinking about where it comes from, and if you want plain text, pipe it into `{{ .Summary | plainify }}`.

Fixes gohugoio#8680
Fixes gohugoio#12761
Fixes gohugoio#12778
Fixes gohugoio#716
  • Loading branch information
bep committed Aug 29, 2024
1 parent 12a28ef commit cbf0dfc
Show file tree
Hide file tree
Showing 21 changed files with 1,536 additions and 799 deletions.
25 changes: 25 additions & 0 deletions common/hugo/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package hugo

import (
"context"
"fmt"
"html/template"
"os"
Expand All @@ -29,6 +30,7 @@ import (
"github.com/mitchellh/mapstructure"

"github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugofs/files"
Expand Down Expand Up @@ -69,6 +71,9 @@ type HugoInfo struct {

conf ConfigProvider
deps []*Dependency

// Context gives access to some of the context scoped variables.
Context Context
}

// Version returns the current version as a comparable version string.
Expand Down Expand Up @@ -127,6 +132,26 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual()
}

type contextKey string

var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))

type Context struct{}

func (c Context) MarkupScope(ctx context.Context) string {
return GetMarkupScope(ctx)
}

// SetMarkupScope sets the markup scope in the context.
func SetMarkupScope(ctx context.Context, s string) context.Context {
return markupScope.Set(ctx, s)
}

// GetMarkupScope gets the markup scope from the context.
func GetMarkupScope(ctx context.Context) string {
return markupScope.Get(ctx)
}

// ConfigProvider represents the config options that are relevant for HugoInfo.
type ConfigProvider interface {
Environment() string
Expand Down
14 changes: 14 additions & 0 deletions common/hugo/hugo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package hugo

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -64,6 +65,19 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
}

func TestMarkupScope(t *testing.T) {
c := qt.New(t)

conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
info := NewInfo(conf, nil)

ctx := context.Background()

ctx = SetMarkupScope(ctx, "foo")

c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
}

type testConfig struct {
environment string
running bool
Expand Down
4 changes: 2 additions & 2 deletions common/paths/pathparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
} else {
high = len(p.s)
}
id := types.LowHigh{Low: i + 1, High: high}
id := types.LowHigh[string]{Low: i + 1, High: high}
if len(p.identifiers) == 0 {
p.identifiers = append(p.identifiers, id)
} else if len(p.identifiers) == 1 {
Expand Down Expand Up @@ -260,7 +260,7 @@ type Path struct {
component string
bundleType PathType

identifiers []types.LowHigh
identifiers []types.LowHigh[string]

posIdentifierLanguage int
disabled bool
Expand Down
12 changes: 10 additions & 2 deletions common/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,20 @@ func Unwrapv(v any) any {
return v
}

// LowHigh is typically used to represent a slice boundary.
type LowHigh struct {
// LowHigh represents a byte or slice boundary.
type LowHigh[S ~[]byte | string] struct {
Low int
High int
}

func (l LowHigh[S]) IsZero() bool {
return l.Low < 0 || (l.Low == 0 && l.High == 0)
}

func (l LowHigh[S]) Value(source S) S {
return source[l.Low:l.High]
}

// This is only used for debugging purposes.
var InvocationCounter atomic.Int64

Expand Down
22 changes: 22 additions & 0 deletions common/types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,25 @@ func TestKeyValues(t *testing.T) {
c.Assert(kv.KeyString(), qt.Equals, "key")
c.Assert(kv.Values, qt.DeepEquals, []any{"a1", "a2"})
}

func TestLowHigh(t *testing.T) {
c := qt.New(t)

lh := LowHigh[string]{
Low: 2,
High: 10,
}

s := "abcdefghijklmnopqrstuvwxyz"
c.Assert(lh.IsZero(), qt.IsFalse)
c.Assert(lh.Value(s), qt.Equals, "cdefghij")

lhb := LowHigh[[]byte]{
Low: 2,
High: 10,
}

sb := []byte(s)
c.Assert(lhb.IsZero(), qt.IsFalse)
c.Assert(lhb.Value(sb), qt.DeepEquals, []byte("cdefghij"))
}
70 changes: 0 additions & 70 deletions helpers/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"html/template"
"strings"
"unicode"
"unicode/utf8"

"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
Expand Down Expand Up @@ -165,75 +164,6 @@ func TotalWords(s string) int {
return n
}

// TruncateWordsByRune truncates words by runes.
func (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) {
words := make([]string, len(in))
copy(words, in)

count := 0
for index, word := range words {
if count >= c.Cfg.SummaryLength() {
return strings.Join(words[:index], " "), true
}
runeCount := utf8.RuneCountInString(word)
if len(word) == runeCount {
count++
} else if count+runeCount < c.Cfg.SummaryLength() {
count += runeCount
} else {
for ri := range word {
if count >= c.Cfg.SummaryLength() {
truncatedWords := append(words[:index], word[:ri])
return strings.Join(truncatedWords, " "), true
}
count++
}
}
}

return strings.Join(words, " "), false
}

// TruncateWordsToWholeSentence takes content and truncates to whole sentence
// limited by max number of words. It also returns whether it is truncated.
func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
var (
wordCount = 0
lastWordIndex = -1
)

for i, r := range s {
if unicode.IsSpace(r) {
wordCount++
lastWordIndex = i

if wordCount >= c.Cfg.SummaryLength() {
break
}

}
}

if lastWordIndex == -1 {
return s, false
}

endIndex := -1

for j, r := range s[lastWordIndex:] {
if isEndOfSentence(r) {
endIndex = j + lastWordIndex + utf8.RuneLen(r)
break
}
}

if endIndex == -1 {
return s, false
}

return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
}

// TrimShortHTML removes the outer tags from HTML input where (a) the opening
// tag is present only once with the input, and (b) the opening and closing
// tags wrap the input after white space removal.
Expand Down
77 changes: 0 additions & 77 deletions helpers/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"testing"

qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
)

Expand Down Expand Up @@ -68,82 +67,6 @@ func TestBytesToHTML(t *testing.T) {

var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20)

func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) {
c := newTestContentSpec(nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.TruncateWordsToWholeSentence(benchmarkTruncateString)
}
}

func TestTruncateWordsToWholeSentence(t *testing.T) {
type test struct {
input, expected string
max int
truncated bool
}
data := []test{
{"a b c", "a b c", 12, false},
{"a b c", "a b c", 3, false},
{"a", "a", 1, false},
{"This is a sentence.", "This is a sentence.", 5, false},
{"This is also a sentence!", "This is also a sentence!", 1, false},
{"To be. Or not to be. That's the question.", "To be.", 1, true},
{" \nThis is not a sentence\nAnd this is another", "This is not a sentence", 4, true},
{"", "", 10, false},
{"This... is a more difficult test?", "This... is a more difficult test?", 1, false},
}
for i, d := range data {
cfg := config.New()
cfg.Set("summaryLength", d.max)
c := newTestContentSpec(cfg)
output, truncated := c.TruncateWordsToWholeSentence(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}

if d.truncated != truncated {
t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated)
}
}
}

func TestTruncateWordsByRune(t *testing.T) {
type test struct {
input, expected string
max int
truncated bool
}
data := []test{
{"", "", 1, false},
{"a b c", "a b c", 12, false},
{"a b c", "a b c", 3, false},
{"a", "a", 1, false},
{"Hello 中国", "", 0, true},
{"这是中文,全中文。", "这是中文,", 5, true},
{"Hello 中国", "Hello 中", 2, true},
{"Hello 中国", "Hello 中国", 3, false},
{"Hello中国 Good 好的", "Hello中国 Good 好", 9, true},
{"This is a sentence.", "This is", 2, true},
{"This is also a sentence!", "This", 1, true},
{"To be. Or not to be. That's the question.", "To be. Or not", 4, true},
{" \nThis is not a sentence\n ", "This is not", 3, true},
}
for i, d := range data {
cfg := config.New()
cfg.Set("summaryLength", d.max)
c := newTestContentSpec(cfg)
output, truncated := c.TruncateWordsByRune(strings.Fields(d.input))
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}

if d.truncated != truncated {
t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated)
}
}
}

func TestExtractTOCNormalContent(t *testing.T) {
content := []byte("<nav>\n<ul>\nTOC<li><a href=\"#")

Expand Down
8 changes: 3 additions & 5 deletions hugolib/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var (
pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType)
nopPageOutput = &pageOutput{
pagePerOutputProviders: nopPagePerOutput,
MarkupProvider: page.NopPage,
ContentProvider: page.NopPage,
}
)
Expand Down Expand Up @@ -213,11 +214,8 @@ func (p *pageHeadingsFiltered) page() page.Page {

// For internal use by the related content feature.
func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document {
r, err := p.m.content.contentToC(ctx, p.pageOutput.pco)
if err != nil {
panic(err)
}
headings := r.tableOfContents.Headings.FilterBy(fn)
fragments := p.pageOutput.pco.c().Fragments(ctx)
headings := fragments.Headings.FilterBy(fn)
return &pageHeadingsFiltered{
pageState: p,
headings: headings,
Expand Down
Loading

0 comments on commit cbf0dfc

Please sign in to comment.