diff options
-rw-r--r-- | hugolib/collections.go | 2 | ||||
-rw-r--r-- | langs/language.go | 11 | ||||
-rw-r--r-- | media/mediaType.go | 3 | ||||
-rw-r--r-- | navigation/menu.go | 1 | ||||
-rw-r--r-- | resources/errorResource.go | 15 | ||||
-rw-r--r-- | resources/image.go | 31 | ||||
-rw-r--r-- | resources/image_test.go | 27 | ||||
-rw-r--r-- | resources/images/exif/exif.go | 16 | ||||
-rw-r--r-- | resources/images/exif/exif_test.go | 2 | ||||
-rw-r--r-- | resources/images/image.go | 2 | ||||
-rw-r--r-- | resources/images/image_resource.go | 53 | ||||
-rw-r--r-- | resources/page/page.go | 3 | ||||
-rw-r--r-- | resources/page/pagegroup.go | 6 | ||||
-rw-r--r-- | resources/page/pages.go | 12 | ||||
-rw-r--r-- | resources/page/site.go | 31 | ||||
-rw-r--r-- | resources/page/weighted.go | 2 | ||||
-rw-r--r-- | resources/resource.go | 2 | ||||
-rw-r--r-- | resources/resource/dates.go | 7 | ||||
-rw-r--r-- | resources/resource/resourcetypes.go | 23 | ||||
-rw-r--r-- | resources/testhelpers_test.go | 9 | ||||
-rw-r--r-- | resources/transform.go | 17 | ||||
-rw-r--r-- | resources/transform_test.go | 3 | ||||
-rw-r--r-- | tpl/images/images.go | 5 |
23 files changed, 193 insertions, 90 deletions
diff --git a/hugolib/collections.go b/hugolib/collections.go index 933f7dadc..898d2ba12 100644 --- a/hugolib/collections.go +++ b/hugolib/collections.go @@ -27,7 +27,7 @@ var ( // here as it makes it easier to get an idea of "type coverage". These // implementations have no value on their own. -// Slice is not meant to be used externally. It's a bridge function +// Slice is for internal use. func (p *pageState) Slice(items any) (any, error) { return page.ToPages(items) } diff --git a/langs/language.go b/langs/language.go index 244f6a743..a0294a103 100644 --- a/langs/language.go +++ b/langs/language.go @@ -55,21 +55,26 @@ type Language struct { Title string Weight int + // For internal use. Disabled bool // If set per language, this tells Hugo that all content files without any // language indicator (e.g. my-page.en.md) is in this language. // This is usually a path relative to the working dir, but it can be an // absolute directory reference. It is what we get. + // For internal use. ContentDir string // Global config. + // For internal use. Cfg config.Provider // Language specific config. + // For internal use. LocalCfg config.Provider // Composite config. + // For internal use. config.Provider // These are params declared in the [params] section of the language merged with the @@ -91,6 +96,7 @@ type Language struct { initErr error } +// For internal use. func (l *Language) String() string { return l.Lang } @@ -233,6 +239,7 @@ func (l Languages) IsMultihost() bool { // SetParam sets a param with the given key and value. // SetParam is case-insensitive. +// For internal use. func (l *Language) SetParam(k string, v any) { l.paramsMu.Lock() defer l.paramsMu.Unlock() @@ -245,6 +252,7 @@ func (l *Language) SetParam(k string, v any) { // GetLocal gets a configuration value set on language level. It will // not fall back to any global value. // It will return nil if a value with the given key cannot be found. +// For internal use. func (l *Language) GetLocal(key string) any { if l == nil { panic("language not set") @@ -256,6 +264,7 @@ func (l *Language) GetLocal(key string) any { return nil } +// For internal use. func (l *Language) Set(k string, v any) { k = strings.ToLower(k) if globalOnlySettings[k] { @@ -265,11 +274,13 @@ func (l *Language) Set(k string, v any) { } // Merge is currently not supported for Language. +// For internal use. func (l *Language) Merge(key string, value any) { panic("Not supported") } // IsSet checks whether the key is set in the language or the related config store. +// For internal use. func (l *Language) IsSet(key string) bool { key = strings.ToLower(key) if !globalOnlySettings[key] { diff --git a/media/mediaType.go b/media/mediaType.go index 1627a9d52..69bb9182a 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -163,6 +163,7 @@ func (m Type) Type() string { return m.MainType + "/" + m.SubType } +// For internal use. func (m Type) String() string { return m.Type() } @@ -510,11 +511,13 @@ func DecodeTypes(mms ...map[string]any) (Types, error) { } // IsZero reports whether this Type represents a zero value. +// For internal use. func (m Type) IsZero() bool { return m.SubType == "" } // MarshalJSON returns the JSON encoding of m. +// For internal use. func (m Type) MarshalJSON() ([]byte, error) { type Alias Type return json.Marshal(&struct { diff --git a/navigation/menu.go b/navigation/menu.go index b9fb46e70..02e1f8fec 100644 --- a/navigation/menu.go +++ b/navigation/menu.go @@ -156,6 +156,7 @@ func (m *MenuEntry) isSamePage(p Page) bool { return false } +// For internal use. func (m *MenuEntry) MarshallMap(ime map[string]any) error { var err error for k, v := range ime { diff --git a/resources/errorResource.go b/resources/errorResource.go index 50f0be371..81375cc48 100644 --- a/resources/errorResource.go +++ b/resources/errorResource.go @@ -19,6 +19,7 @@ import ( "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/resources/images" "github.com/gohugoio/hugo/resources/images/exif" "github.com/gohugoio/hugo/resources/resource" ) @@ -26,7 +27,7 @@ import ( var ( _ error = (*errorResource)(nil) // Imnage covers all current Resource implementations. - _ resource.Image = (*errorResource)(nil) + _ images.ImageResource = (*errorResource)(nil) // The list of user facing and exported interfaces in resource.go // Note that if we're missing some interface here, the user will still // get an error, but not as pretty. @@ -98,27 +99,27 @@ func (e *errorResource) Width() int { panic(e.ResourceError) } -func (e *errorResource) Crop(spec string) (resource.Image, error) { +func (e *errorResource) Crop(spec string) (images.ImageResource, error) { panic(e.ResourceError) } -func (e *errorResource) Fill(spec string) (resource.Image, error) { +func (e *errorResource) Fill(spec string) (images.ImageResource, error) { panic(e.ResourceError) } -func (e *errorResource) Fit(spec string) (resource.Image, error) { +func (e *errorResource) Fit(spec string) (images.ImageResource, error) { panic(e.ResourceError) } -func (e *errorResource) Resize(spec string) (resource.Image, error) { +func (e *errorResource) Resize(spec string) (images.ImageResource, error) { panic(e.ResourceError) } -func (e *errorResource) Filter(filters ...any) (resource.Image, error) { +func (e *errorResource) Filter(filters ...any) (images.ImageResource, error) { panic(e.ResourceError) } -func (e *errorResource) Exif() *exif.Exif { +func (e *errorResource) Exif() *exif.ExifInfo { panic(e.ResourceError) } diff --git a/resources/image.go b/resources/image.go index 86cc3251a..253caf735 100644 --- a/resources/image.go +++ b/resources/image.go @@ -49,12 +49,12 @@ import ( ) var ( - _ resource.Image = (*imageResource)(nil) - _ resource.Source = (*imageResource)(nil) - _ resource.Cloner = (*imageResource)(nil) + _ images.ImageResource = (*imageResource)(nil) + _ resource.Source = (*imageResource)(nil) + _ resource.Cloner = (*imageResource)(nil) ) -// ImageResource represents an image resource. +// imageResource represents an image resource. type imageResource struct { *images.Image @@ -70,14 +70,14 @@ type imageResource struct { } type imageMeta struct { - Exif *exif.Exif + Exif *exif.ExifInfo } -func (i *imageResource) Exif() *exif.Exif { +func (i *imageResource) Exif() *exif.ExifInfo { return i.root.getExif() } -func (i *imageResource) getExif() *exif.Exif { +func (i *imageResource) getExif() *exif.ExifInfo { i.metaInit.Do(func() { supportsExif := i.Format == images.JPEG || i.Format == images.TIFF if !supportsExif { @@ -137,6 +137,7 @@ func (i *imageResource) getExif() *exif.Exif { return i.meta.Exif } +// Cloneis for internal use. func (i *imageResource) Clone() resource.Resource { gr := i.baseResource.Clone().(baseResource) return &imageResource{ @@ -170,7 +171,7 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource, // Resize resizes the image to the specified width and height using the specified resampling // filter and returns the transformed image. If one of width or height is 0, the image aspect // ratio is preserved. -func (i *imageResource) Resize(spec string) (resource.Image, error) { +func (i *imageResource) Resize(spec string) (images.ImageResource, error) { conf, err := i.decodeImageConfig("resize", spec) if err != nil { return nil, err @@ -182,8 +183,8 @@ func (i *imageResource) Resize(spec string) (resource.Image, error) { } // Crop the image to the specified dimensions without resizing using the given anchor point. -// Space delimited config: 200x300 TopLeft -func (i *imageResource) Crop(spec string) (resource.Image, error) { +// Space delimited config, e.g. `200x300 TopLeft`. +func (i *imageResource) Crop(spec string) (images.ImageResource, error) { conf, err := i.decodeImageConfig("crop", spec) if err != nil { return nil, err @@ -196,7 +197,7 @@ func (i *imageResource) Crop(spec string) (resource.Image, error) { // Fit scales down the image using the specified resample filter to fit the specified // maximum width and height. -func (i *imageResource) Fit(spec string) (resource.Image, error) { +func (i *imageResource) Fit(spec string) (images.ImageResource, error) { conf, err := i.decodeImageConfig("fit", spec) if err != nil { return nil, err @@ -209,8 +210,8 @@ func (i *imageResource) Fit(spec string) (resource.Image, error) { // Fill scales the image to the smallest possible size that will cover the specified dimensions, // crops the resized image to the specified dimensions using the given anchor point. -// Space delimited config: 200x300 TopLeft -func (i *imageResource) Fill(spec string) (resource.Image, error) { +// Space delimited config, e.g. `200x300 TopLeft`. +func (i *imageResource) Fill(spec string) (images.ImageResource, error) { conf, err := i.decodeImageConfig("fill", spec) if err != nil { return nil, err @@ -238,7 +239,7 @@ func (i *imageResource) Fill(spec string) (resource.Image, error) { return img, err } -func (i *imageResource) Filter(filters ...any) (resource.Image, error) { +func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) { conf := images.GetDefaultImageConfig("filter", i.Proc.Cfg) var gfilters []gift.Filter @@ -264,7 +265,7 @@ const imageProcWorkers = 1 var imageProcSem = make(chan bool, imageProcWorkers) -func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (resource.Image, error) { +func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (images.ImageResource, error) { img, err := i.getSpec().imageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) { imageProcSem <- true defer func() { diff --git a/resources/image_test.go b/resources/image_test.go index a7577e0a6..0bfef1db0 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -40,7 +40,6 @@ import ( "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/images" - "github.com/gohugoio/hugo/resources/resource" "github.com/google/go-cmp/cmp" "github.com/gohugoio/hugo/htesting/hqt" @@ -76,7 +75,7 @@ func TestImageTransformBasic(t *testing.T) { fileCache := image.(specProvider).getSpec().FileCaches.ImageCache().Fs - assertWidthHeight := func(img resource.Image, w, h int) { + assertWidthHeight := func(img images.ImageResource, w, h int) { c.Helper() c.Assert(img, qt.Not(qt.IsNil)) c.Assert(img.Width(), qt.Equals, w) @@ -162,7 +161,7 @@ func TestImageTransformFormat(t *testing.T) { fileCache := image.(specProvider).getSpec().FileCaches.ImageCache().Fs - assertExtWidthHeight := func(img resource.Image, ext string, w, h int) { + assertExtWidthHeight := func(img images.ImageResource, ext string, w, h int) { c.Helper() c.Assert(img, qt.Not(qt.IsNil)) c.Assert(paths.Ext(img.RelPermalink()), qt.Equals, ext) @@ -210,13 +209,13 @@ func TestImagePermalinkPublishOrder(t *testing.T) { os.Remove(workDir) }() - check1 := func(img resource.Image) { + check1 := func(img images.ImageResource) { resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg" c.Assert(img.RelPermalink(), qt.Equals, resizedLink) assertImageFile(c, spec.PublishFs, resizedLink, 100, 50) } - check2 := func(img resource.Image) { + check2 := func(img images.ImageResource) { c.Assert(img.RelPermalink(), qt.Equals, "/a/sunset.jpg") assertImageFile(c, spec.PublishFs, "a/sunset.jpg", 900, 562) } @@ -231,7 +230,7 @@ func TestImagePermalinkPublishOrder(t *testing.T) { resized, err := orignal.Resize("100x50") c.Assert(err, qt.IsNil) - check1(resized.(resource.Image)) + check1(resized.(images.ImageResource)) if !checkOriginalFirst { check2(orignal) @@ -441,9 +440,9 @@ func TestImageExif(t *testing.T) { c := qt.New(t) fs := afero.NewMemMapFs() spec := newTestResourceSpec(specDescriptor{fs: fs, c: c}) - image := fetchResourceForSpec(spec, c, "sunset.jpg").(resource.Image) + image := fetchResourceForSpec(spec, c, "sunset.jpg").(images.ImageResource) - getAndCheckExif := func(c *qt.C, image resource.Image) { + getAndCheckExif := func(c *qt.C, image images.ImageResource) { x := image.Exif() c.Assert(x, qt.Not(qt.IsNil)) @@ -464,22 +463,22 @@ func TestImageExif(t *testing.T) { } getAndCheckExif(c, image) - image = fetchResourceForSpec(spec, c, "sunset.jpg").(resource.Image) + image = fetchResourceForSpec(spec, c, "sunset.jpg").(images.ImageResource) // This will read from file cache. getAndCheckExif(c, image) } func BenchmarkImageExif(b *testing.B) { - getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []resource.Image { + getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource { spec := newTestResourceSpec(specDescriptor{fs: fs, c: c}) - images := make([]resource.Image, b.N) + imgs := make([]images.ImageResource, b.N) for i := 0; i < b.N; i++ { - images[i] = fetchResourceForSpec(spec, c, "sunset.jpg", strconv.Itoa(i)).(resource.Image) + imgs[i] = fetchResourceForSpec(spec, c, "sunset.jpg", strconv.Itoa(i)).(images.ImageResource) } - return images + return imgs } - getAndCheckExif := func(c *qt.C, image resource.Image) { + getAndCheckExif := func(c *qt.C, image images.ImageResource) { x := image.Exif() c.Assert(x, qt.Not(qt.IsNil)) c.Assert(x.Long, qt.Equals, float64(-4.50846)) diff --git a/resources/images/exif/exif.go b/resources/images/exif/exif.go index 2841bd153..487f250d5 100644 --- a/resources/images/exif/exif.go +++ b/resources/images/exif/exif.go @@ -32,10 +32,18 @@ import ( const exifTimeLayout = "2006:01:02 15:04:05" -type Exif struct { - Lat float64 +// ExifInfo holds the decoded Exif data for an Image. +type ExifInfo struct { + // GPS latitude in degrees. + Lat float64 + + // GPS longitude in degrees. Long float64 + + // Image creation date/time. Date time.Time + + // A collection of the available Exif tags for this Image. Tags Tags } @@ -106,7 +114,7 @@ func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) { return d, nil } -func (d *Decoder) Decode(r io.Reader) (ex *Exif, err error) { +func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("Exif failed: %v", r) @@ -139,7 +147,7 @@ func (d *Decoder) Decode(r io.Reader) (ex *Exif, err error) { return } - ex = &Exif{Lat: lat, Long: long, Date: tm, Tags: walker.vals} + ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals} return } diff --git a/resources/images/exif/exif_test.go b/resources/images/exif/exif_test.go index 0cb9f6704..cd5961404 100644 --- a/resources/images/exif/exif_test.go +++ b/resources/images/exif/exif_test.go @@ -56,7 +56,7 @@ func TestExif(t *testing.T) { // Verify that it survives a round-trip to JSON and back. data, err := json.Marshal(x) c.Assert(err, qt.IsNil) - x2 := &Exif{} + x2 := &ExifInfo{} err = json.Unmarshal(data, x2) c.Assert(x2, eq, x) diff --git a/resources/images/image.go b/resources/images/image.go index 25deda925..e8cca7769 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -192,7 +192,7 @@ type ImageProcessor struct { exifDecoder *exif.Decoder } -func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.Exif, error) { +func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { return p.exifDecoder.Decode(r) } diff --git a/resources/images/image_resource.go b/resources/images/image_resource.go new file mode 100644 index 000000000..e0fec15a0 --- /dev/null +++ b/resources/images/image_resource.go @@ -0,0 +1,53 @@ +// Copyright 2022 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 images + +import ( + "image" + + "github.com/gohugoio/hugo/resources/images/exif" + "github.com/gohugoio/hugo/resources/resource" +) + +// ImageResource represents an image resource. +type ImageResource interface { + resource.Resource + ImageResourceOps +} + +type ImageResourceOps interface { + // Height returns the height of the Image. + Height() int + // Width returns the width of the Image. + Width() int + + // Crop an image to match the given dimensions without resizing. + // You must provide both width and height. + // Use the anchor option to change the crop box anchor point. + // {{ $image := $image.Crop "600x400" }} + Crop(spec string) (ImageResource, error) + Fill(spec string) (ImageResource, error) + Fit(spec string) (ImageResource, error) + Resize(spec string) (ImageResource, error) + + // Filter applies one or more filters to an Image. + // {{ $image := $image.Filter (images.GaussianBlur 6) (images.Pixelate 8) }} + Filter(filters ...any) (ImageResource, error) + + // Exif returns an ExifInfo object containing Image metadata. + Exif() *exif.ExifInfo + + // Internal + DecodeImage() (image.Image, error) +} diff --git a/resources/page/page.go b/resources/page/page.go index 5ddacc8a6..50459c465 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -161,8 +161,7 @@ type PageMetaProvider interface { // Aliases forms the base for redirects generation. Aliases() []string - // BundleType returns the bundle type: "leaf", "branch" or an empty string if it is none. - // See https://gohugo.io/content-management/page-bundles/ + // BundleType returns the bundle type: `leaf`, `branch` or an empty string. BundleType() files.ContentClass // A configured description. diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go index 1d9827907..3b32a1fae 100644 --- a/resources/page/pagegroup.go +++ b/resources/page/pagegroup.go @@ -40,7 +40,10 @@ var ( // PageGroup represents a group of pages, grouped by the key. // The key is typically a year or similar. type PageGroup struct { + // The key, typically a year or similar. Key any + + // The Pages in this group. Pages } @@ -361,6 +364,7 @@ func (p Pages) GroupByParamDate(key string, format string, order ...string) (Pag } // ProbablyEq wraps compare.ProbablyEqer +// For internal use. func (p PageGroup) ProbablyEq(other any) bool { otherP, ok := other.(PageGroup) if !ok { @@ -374,7 +378,7 @@ func (p PageGroup) ProbablyEq(other any) bool { return p.Pages.ProbablyEq(otherP.Pages) } -// Slice is not meant to be used externally. It's a bridge function +// Slice is for internal use. // for the template functions. See collections.Slice. func (p PageGroup) Slice(in any) (any, error) { switch items := in.(type) { diff --git a/resources/page/pages.go b/resources/page/pages.go index 4db6a4f68..f47af5114 100644 --- a/resources/page/pages.go +++ b/resources/page/pages.go @@ -22,9 +22,11 @@ import ( "github.com/gohugoio/hugo/resources/resource" ) -// Pages is a slice of pages. This is the most common list type in Hugo. +// Pages is a slice of Page objects. This is the most common list type in Hugo. type Pages []Page +// String returns a string representation of the list. +// For internal use. func (ps Pages) String() string { return fmt.Sprintf("Pages(%d)", len(ps)) } @@ -37,7 +39,8 @@ func (ps Pages) shuffle() { } } -// ToResources wraps resource.ResourcesConverter +// ToResources wraps resource.ResourcesConverter. +// For internal use. func (pages Pages) ToResources() resource.Resources { r := make(resource.Resources, len(pages)) for i, p := range pages { @@ -86,10 +89,12 @@ func ToPages(seq any) (Pages, error) { return nil, fmt.Errorf("cannot convert type %T to Pages", seq) } +// Group groups the pages in in by key. +// This implements collections.Grouper. func (p Pages) Group(key any, in any) (any, error) { pages, err := ToPages(in) if err != nil { - return nil, err + return PageGroup{}, err } return PageGroup{Key: key, Pages: pages}, nil } @@ -100,6 +105,7 @@ func (p Pages) Len() int { } // ProbablyEq wraps compare.ProbablyEqer +// For internal use. func (pages Pages) ProbablyEq(other any) bool { otherPages, ok := other.(Pages) if !ok { diff --git a/resources/page/site.go b/resources/page/site.go index b84f17914..f5806280c 100644 --- a/resources/page/site.go +++ b/resources/page/site.go @@ -29,21 +29,52 @@ import ( // Site represents a site in the build. This is currently a very narrow interface, // but the actual implementation will be richer, see hugolib.SiteInfo. type Site interface { + // Returns the Language configured for this Site. Language() *langs.Language + + // Returns all the regular Pages in this Site. RegularPages() Pages + + // Returns all Pages in this Site. Pages() Pages + + // A shortcut to the home page. Home() Page + + // Returns true if we're running in a server. IsServer() bool + + // Returns the server port. ServerPort() int + + // Returns the configured title for this Site. Title() string + + // Returns all Sites for all languages. Sites() Sites + + // Returns Site currently rendering. Current() Site + + // Returns a struct with some information about the build. Hugo() hugo.Info + + // Returns the BaseURL for this Site. BaseURL() template.URL + + // Retuns a taxonomy map. Taxonomies() any + + // Returns the last modification date of the content. LastChange() time.Time + + // Returns the Menus for this site. Menus() navigation.Menus + + // Returns the Params configured for this site. Params() maps.Params + + // Returns a map of all the data inside /data. Data() map[string]any } diff --git a/resources/page/weighted.go b/resources/page/weighted.go index 5ae2636d4..39034d26c 100644 --- a/resources/page/weighted.go +++ b/ |