Skip to content

Commit

Permalink
Role based output
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Sep 15, 2024
1 parent 28f621d commit b823e3c
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 18 deletions.
10 changes: 10 additions & 0 deletions common/paths/pathparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package paths

import (
"fmt"
"path"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -576,3 +577,12 @@ func HasExt(p string) bool {
}
return false
}

// ValidateIdentifier returns true if the given string is a valid identifier according
// to Hugo's basic path normalization rules.
func ValidateIdentifier(s string) error {
if s == NormalizePathStringBasic(s) {
return nil
}
return fmt.Errorf("must be all lower case and no spaces")
}
4 changes: 4 additions & 0 deletions config/allconfig/allconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
Expand Down Expand Up @@ -137,6 +138,9 @@ type Config struct {
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`

// The roles configuration section contains the top level roles configuration options.
Roles *config.ConfigNamespace[map[string]roles.RoleConfig, roles.Roles] `mapstructure:"-"`

// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
// This can be overridden in the front matter.
Outputs map[string][]string `mapstructure:"-"`
Expand Down
11 changes: 11 additions & 0 deletions config/allconfig/alldecoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
Expand Down Expand Up @@ -201,6 +202,16 @@ var allDecoderSetups = map[string]decodeWeight{
return err
},
},
"roles": {
key: "roles",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Roles, err = roles.DecodeConfig(m)
return err
},
},

"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
Expand Down
2 changes: 2 additions & 0 deletions config/allconfig/configlanguage.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ func (c ConfigLanguage) GetConfigSection(s string) any {
return c.config.MediaTypes.Config
case "outputFormats":
return c.config.OutputFormats.Config
case "roles":
return c.config.Roles.Config
case "permalinks":
return c.config.Permalinks
case "minify":
Expand Down
9 changes: 9 additions & 0 deletions hugofs/glob/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/gobwas/glob"
"github.com/gobwas/glob/syntax"
"github.com/gohugoio/hugo/common/maps"
)

const filepathSeparator = string(os.PathSeparator)
Expand All @@ -33,6 +34,8 @@ var (
isWindows: isWindows,
cache: make(map[string]globErr),
}

simpleGlobCache = maps.NewCache[string, glob.Glob]()
)

type globErr struct {
Expand Down Expand Up @@ -122,6 +125,12 @@ func (g globDecorator) Match(s string) bool {
return g.g.Match(s)
}

func GetGlobSimple(pattern string) (glob.Glob, error) {
return simpleGlobCache.GetOrCreate(pattern, func() (glob.Glob, error) {
return glob.Compile(pattern, '/')
})
}

func GetGlob(pattern string) (glob.Glob, error) {
return defaultGlobCache.GetGlob(pattern)
}
Expand Down
2 changes: 1 addition & 1 deletion hugolib/content_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
Watching: s.Conf.Watching(),
HandlePage: func(pt *pagesfromdata.PagesFromTemplate, pc *pagemeta.PageConfig) error {
s := pt.Site.(*Site)
if err := pc.Compile(pt.GoTmplFi.Meta().PathInfo.Base(), true, "", s.Log, s.conf.MediaTypes.Config); err != nil {
if err := pc.Compile(pt.GoTmplFi.Meta().PathInfo.Base(), true, "", s.Log, s.Conf); err != nil {
return err
}

Expand Down
20 changes: 10 additions & 10 deletions hugolib/content_map_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -891,20 +891,20 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) (contentNodeI, conten
}
}

func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *pageMap {
func newPageMap(sitei int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *pageMap {
var m *pageMap

var taxonomiesConfig taxonomiesConfig = s.conf.Taxonomies

m = &pageMap{
pageTrees: pageTrees.Shape(0, i),
cachePages1: dynacache.GetOrCreatePartition[string, page.Pages](mcache, fmt.Sprintf("/pag1/%d", i), dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}),
cachePages2: dynacache.GetOrCreatePartition[string, page.Pages](mcache, fmt.Sprintf("/pag2/%d", i), dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}),
cacheGetTerms: dynacache.GetOrCreatePartition[string, map[string]page.Pages](mcache, fmt.Sprintf("/gett/%d", i), dynacache.OptionsPartition{Weight: 5, ClearWhen: dynacache.ClearOnRebuild}),
cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources](mcache, fmt.Sprintf("/ress/%d", i), dynacache.OptionsPartition{Weight: 60, ClearWhen: dynacache.ClearOnRebuild}),
cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
pageTrees: pageTrees.Shape(doctree.DimensionLanguage.Index(), sitei),
cachePages1: dynacache.GetOrCreatePartition[string, page.Pages](mcache, fmt.Sprintf("/pag1/%d", sitei), dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}),
cachePages2: dynacache.GetOrCreatePartition[string, page.Pages](mcache, fmt.Sprintf("/pag2/%d", sitei), dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}),
cacheGetTerms: dynacache.GetOrCreatePartition[string, map[string]page.Pages](mcache, fmt.Sprintf("/gett/%d", sitei), dynacache.OptionsPartition{Weight: 5, ClearWhen: dynacache.ClearOnRebuild}),
cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources](mcache, fmt.Sprintf("/ress/%d", sitei), dynacache.OptionsPartition{Weight: 60, ClearWhen: dynacache.ClearOnRebuild}),
cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", sitei), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", sitei), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", sitei), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),

contentDataFileSeenItems: maps.NewCache[string, map[uint64]bool](),

Expand All @@ -915,7 +915,7 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *
taxonomyTermDisabled: !s.conf.IsKindEnabled(kinds.KindTerm),
pageDisabled: !s.conf.IsKindEnabled(kinds.KindPage),
},
i: i,
i: sitei,
s: s,
}

Expand Down
7 changes: 4 additions & 3 deletions hugolib/doctree/dimensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
package doctree

const (
// Language is currently the only dimension in the Hugo build matrix.
// Dimensions in the Hugo build matrix.
DimensionLanguage DimensionFlag = 1 << iota
DimensionRole
)

// Dimension is a row in the Hugo build matrix which currently has one value: language.
type Dimension [1]int
// Dimension is a row in the Hugo build matrix: language and role.
type Dimension [2]int

// DimensionFlag is a flag in the Hugo build matrix.
type DimensionFlag byte
Expand Down
5 changes: 4 additions & 1 deletion hugolib/page__meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ params:
case "keywords":
pcfg.Keywords = cast.ToStringSlice(v)
params[loki] = pcfg.Keywords
case "roles":
pcfg.Roles = cast.ToStringSlice(v)
params[loki] = pcfg.Roles
case "headless":
// Legacy setting for leaf bundles.
// This is since Hugo 0.63 handled in a more general way for all
Expand Down Expand Up @@ -688,7 +691,7 @@ params:
return err
}

if err := pcfg.Compile("", false, ext, p.s.Log, p.s.conf.MediaTypes.Config); err != nil {
if err := pcfg.Compile("", false, ext, p.s.Log, p.s.Conf); err != nil {
return err
}

Expand Down
144 changes: 144 additions & 0 deletions hugolib/roles/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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 roles

import (
"errors"
"fmt"
"sort"

"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/mitchellh/mapstructure"
)

type RoleConfig struct {
// Whether this role is the default role.
// This will be rendered in the root.
// There can only be one default role.
Default bool

// The weight of the role.
// Used to determine the order of the roles.
// If zero, we use the role name.
Weight int
}

type Role struct {
Name string
RoleConfig
}

type Roles struct {
roleConfigs map[string]RoleConfig
Sorted []Role
}

func (r Roles) IndexDefault() int {
for i, role := range r.Sorted {
if role.Default {
return i
}
}
panic("no default role found")
}

// IndexMatch returns the index of the first role that matches the given Glob pattern.
func (r Roles) IndexMatch(pattern string) (int, error) {
g, err := glob.GetGlob(pattern)
if err != nil {
return 0, err
}
for i, role := range r.Sorted {
if g.Match(role.Name) {
return i, nil
}
}
return -1, nil
}

func (r *Roles) init() error {
if len(r.roleConfigs) == 0 {
// Add a default role.
r.roleConfigs["guest"] = RoleConfig{Default: true}
}

var defaultSeen int
for k, v := range r.roleConfigs {
if k == "" {
return errors.New("role name cannot be empty")
}

if err := paths.ValidateIdentifier(k); err != nil {
// TODO1 config keys gets auto lowercased, so this will (almost) never happen.
return fmt.Errorf("role name %q is invalid: %s", k, err)
}

if v.Default {
defaultSeen++
}

if defaultSeen > 1 {
return errors.New("only one role can be the default role")
}

r.Sorted = append(r.Sorted, Role{Name: k, RoleConfig: v})
}

// Sort by weight if set, then by name.
sort.SliceStable(r.Sorted, func(i, j int) bool {
ri, rj := r.Sorted[i], r.Sorted[j]
if ri.Weight == rj.Weight {
return ri.Name < rj.Name
}
if rj.Weight == 0 {
return true
}
if ri.Weight == 0 {
return false
}
return ri.Weight < rj.Weight
})

if defaultSeen == 0 {
// If no default role is set, we set the first one.
first := r.Sorted[0]
first.Default = true
r.roleConfigs[first.Name] = first.RoleConfig
r.Sorted[0] = first
}

return nil
}

func (r Roles) Has(role string) bool {
_, found := r.roleConfigs[role]
return found
}

func DecodeConfig(m map[string]any) (*config.ConfigNamespace[map[string]RoleConfig, Roles], error) {
return config.DecodeNamespace[map[string]RoleConfig](m, func(in any) (Roles, any, error) {
var roles Roles
var conf map[string]RoleConfig
if err := mapstructure.Decode(m, &conf); err != nil {
return roles, nil, err
}
roles.roleConfigs = conf
if err := roles.init(); err != nil {
return roles, nil, err
}
return roles, nil, nil
})
}
53 changes: 53 additions & 0 deletions hugolib/roles/roles_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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 roles_test

import (
"testing"

"github.com/gohugoio/hugo/hugolib"
)

// TODO1 role hierarchy.
func TestRoles(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
[roles]
[roles.guest]
default = true
weight = 100
[roles.member]
weight = 200
-- content/memberonly.md --
---
title: "Member Only"
roles: ["member"]
---
Member content.
-- content/public.md --
---
title: "Public"
---
Users with no (blank) role will see this.
-- layouts/_default/single.html --
{{ .Title }}|{{ .Content }}|
`

b := hugolib.Test(t, files)

b.AssertPublishDir("adsf")
}
1 change: 1 addition & 0 deletions hugolib/site_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Site struct {
conf *allconfig.Config
language *langs.Language
languagei int
rolei int
pageMap *pageMap

// The owning container.
Expand Down
Loading

0 comments on commit b823e3c

Please sign in to comment.