Compare commits

...

22 commits

Author SHA1 Message Date
pagdot e027192787
Merge fbbcca40c8 into 11aa893198 2024-04-19 12:06:55 +05:30
Joe Mooring 11aa893198
commands: Provide examples for chromastyles flags
Closes #12387
2024-04-18 12:16:36 -07:00
hugoreleaser d88cb5269a releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-18 08:34:20 +00:00
hugoreleaser 68c5ad638c releaser: Bump versions for release of 0.125.1
[ci skip]
2024-04-18 08:21:19 +00:00
Bjørn Erik Pedersen 0c188fda24 tpl: Use erroridf for remote YouTube errors
So they can be silenced.

Fixes #12383
2024-04-18 10:02:36 +02:00
Bjørn Erik Pedersen bbc6888d02
build: Fix `GLIBC_2.29' not found issue
Closes #12381
2024-04-17 12:04:00 +02:00
hugoreleaser 8c14d1edc3 releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-16 15:21:02 +00:00
hugoreleaser a32400b5f4 releaser: Bump versions for release of 0.125.0
[ci skip]
2024-04-16 15:04:41 +00:00
Bjørn Erik Pedersen df9f2fb617
docs: Regen docshelper 2024-04-16 12:08:28 +02:00
Bjørn Erik Pedersen fa60a2fbc3 Fix server rebuilds when adding a content file on Linux
Fixes #12362
2024-04-16 12:06:37 +02:00
dependabot[bot] fe63de3a83 build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-16 10:43:50 +02:00
Bjørn Erik Pedersen e197c7b29d Add Luminance to Color
To sort an image's colors from darkest to lightest, you can then do:

```handlebars
{{ {{ $colorsByLuminance := sort $image.Colors "Luminance" }}
```

This uses the formula defined here: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance

Fixes #10450
2024-04-16 10:02:46 +02:00
Paul fbbcca40c8 add integration test to test toc generation 2023-10-09 15:52:43 +02:00
Paul addc311e56 fix convert.go for changes made in other parts 2023-10-09 15:52:25 +02:00
pagdot 6c8f472712
Merge branch 'master' into pandoc-toc 2023-10-09 10:25:12 +02:00
Paul c38633a0c4
add basic integrations tests to pandoc 2022-10-06 19:13:37 +02:00
Paul 35461c167f
fix parsing of pandoc result 2022-10-06 19:13:28 +02:00
Paul aaed290b02
fix compile error from rebase and improve error handling 2022-10-06 19:04:44 +02:00
Paul f1b1e5f41b
remove generated title, update trim positions 2022-10-06 19:04:44 +02:00
Paul 769439cc5b
Add dummy title
Else pandoc complains about missing title
2022-10-06 19:04:44 +02:00
Paul 6de4f5cde5
Use pandoc's default standalone template
Extract body from template first
2022-10-06 19:04:44 +02:00
Paul d362fae970
Add basic toc generation for pandoc (WIP)
Pandoc template missing right now
2022-10-06 19:04:44 +02:00
28 changed files with 611 additions and 87 deletions

View file

@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22200.20200
- image: bepsays/ci-hugoreleaser:1.22200.20201
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@ -60,7 +60,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20200
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20201
steps:
- *restore-cache
- &attach-workspace

View file

@ -78,9 +78,9 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", `foreground and background colors for inline line numbers, e.g. --linesStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("linesStyle", cobra.NoFileCompletions)
},
}

View file

@ -123,6 +123,20 @@ func InSlicEqualFold(arr []string, el string) bool {
return false
}
// ToString converts the given value to a string.
// Note that this is a more strict version compared to cast.ToString,
// as it will not try to convert numeric values to strings,
// but only accept strings or fmt.Stringer.
func ToString(v any) (string, bool) {
switch vv := v.(type) {
case string:
return vv, true
case fmt.Stringer:
return vv.String(), true
}
return "", false
}
type Tuple struct {
First string
Second string

View file

@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 125,
Minor: 126,
PatchLevel: 0,
Suffix: "-DEV",
}

View file

@ -4017,6 +4017,11 @@ tpl:
- s
Description: CountWords returns the approximate word count in s.
Examples: []
Diff:
Aliases: null
Args: null
Description: ""
Examples: null
FindRE:
Aliases:
- findRE

2
go.mod
View file

@ -55,7 +55,7 @@ require (
github.com/niklasfasching/go-org v1.7.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.2.0
github.com/pelletier/go-toml/v2 v2.2.1
github.com/rogpeppe/go-internal v1.12.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sanity-io/litter v1.5.5

4
go.sum
View file

@ -376,8 +376,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=

View file

@ -610,7 +610,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
// For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/.
events = h.fileEventsFilter(events)
events = h.fileEventsTranslate(events)
events = h.fileEventsTrim(events)
eventInfos := h.fileEventsApplyInfo(events)
logger := h.Log

View file

@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
@ -685,8 +686,17 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
return nil
}
// We simulate the fsnotify events.
// See the test output in https://github.com/bep/fsnotifyeventlister for what events gets produced
// by the different OSes.
func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
var events []fsnotify.Event
var (
events []fsnotify.Event
isLinux = runtime.GOOS == "linux"
isMacOs = runtime.GOOS == "darwin"
isWindows = runtime.GOOS == "windows"
)
for _, v := range s.removedFiles {
events = append(events, fsnotify.Event{
Name: v,
@ -713,12 +723,32 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
Name: v,
Op: fsnotify.Write,
})
if isLinux || isWindows {
// Duplicate write events, for some reason.
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
if isMacOs {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Chmod,
})
}
}
for _, v := range s.createdFiles {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Create,
})
if isLinux || isWindows {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
}
// Shuffle events.

View file

@ -1553,3 +1553,27 @@ Single: {{ .Title }}|{{ .Content }}|
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildEditSingleListChangeUbuntuIssue12362(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','section','sitemap','taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
{{ range .Pages }}{{ .Title }}|{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
-- content/p1.md --
---
title: p1
---
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "p1|")
b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build()
b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality
}

View file

@ -424,7 +424,35 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
events[n] = ev
n++
}
return events[:n]
events = events[:n]
eventOrdinal := func(e fsnotify.Event) int {
// Pull the structural changes to the top.
if e.Op.Has(fsnotify.Create) {
return 1
}
if e.Op.Has(fsnotify.Remove) {
return 2
}
if e.Op.Has(fsnotify.Rename) {
return 3
}
if e.Op.Has(fsnotify.Write) {
return 4
}
return 5
}
sort.Slice(events, func(i, j int) bool {
// First sort by event type.
if eventOrdinal(events[i]) != eventOrdinal(events[j]) {
return eventOrdinal(events[i]) < eventOrdinal(events[j])
}
// Then sort by name.
return events[i].Name < events[j].Name
})
return events
}
type fileEventInfo struct {
@ -494,41 +522,17 @@ func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo
return infos
}
func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
eventMap := make(map[string][]fsnotify.Event)
// We often get a Remove etc. followed by a Create, a Create followed by a Write.
// Remove the superfluous events to make the update logic simpler.
for _, ev := range events {
eventMap[ev.Name] = append(eventMap[ev.Name], ev)
}
func (h *HugoSites) fileEventsTrim(events []fsnotify.Event) []fsnotify.Event {
seen := make(map[string]bool)
n := 0
for _, ev := range events {
mapped := eventMap[ev.Name]
// Keep one
found := false
var kept fsnotify.Event
for i, ev2 := range mapped {
if i == 0 {
kept = ev2
}
if ev2.Op&fsnotify.Write == fsnotify.Write {
kept = ev2
found = true
}
if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
kept = ev2
}
if seen[ev.Name] {
continue
}
events[n] = kept
seen[ev.Name] = true
events[n] = ev
n++
}
return events
}

View file

@ -1,7 +1,9 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.124.1
HUGORELEASER_COMMITISH=db083b05f16c945fec04f745f0ca8640560cf1ec
HUGORELEASER_TAG=v0.125.1
HUGORELEASER_COMMITISH=68c5ad638c2072969e47262926b912e80fd71a77

View file

@ -19,6 +19,7 @@ import (
"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/pandoc/pandoc_config"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/mitchellh/mapstructure"
)
@ -39,6 +40,7 @@ type Config struct {
// Configuration for the Asciidoc external markdown engine.
AsciidocExt asciidocext_config.Config
Pandoc pandoc_config.Config
}
func Decode(cfg config.Provider) (conf Config, err error) {
@ -105,4 +107,5 @@ var Default = Config{
Goldmark: goldmark_config.Default,
AsciidocExt: asciidocext_config.Default,
Pandoc: pandoc_config.Default,
}

View file

@ -15,12 +15,16 @@
package pandoc
import (
"bytes"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/identity"
"golang.org/x/net/html"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/tableofcontents"
)
// Provider is the package entry point.
@ -37,17 +41,33 @@ func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error)
}), nil
}
type pandocResult struct {
converter.ResultRender
toc *tableofcontents.Fragments
}
func (r pandocResult) TableOfContents() *tableofcontents.Fragments {
return r.toc
}
type pandocConverter struct {
ctx converter.DocumentContext
cfg converter.ProviderConfig
}
func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
b, err := c.getPandocContent(ctx.Src, c.ctx)
contentWithToc, err := c.getPandocContent(ctx.Src, c.ctx)
if err != nil {
return nil, err
}
return converter.Bytes(b), nil
content, toc, err := c.extractTOC(contentWithToc)
if err != nil {
return nil, err
}
return pandocResult{
ResultRender: converter.Bytes(content),
toc: toc,
}, nil
}
func (c *pandocConverter) Supports(feature identity.Identity) bool {
@ -63,7 +83,7 @@ func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentCon
" Leaving pandoc content unrendered.")
return src, nil
}
args := []string{"--mathjax"}
args := []string{"--mathjax", "--toc", "-s", "--metadata", "title=dummy"}
return internal.ExternallyRenderContent(c.cfg, ctx, src, binaryName, args)
}
@ -76,6 +96,164 @@ func getPandocBinaryName() string {
return ""
}
// extractTOC extracts the toc from the given src html.
// It returns the html without the TOC, and the TOC data
func (a *pandocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
var buf bytes.Buffer
buf.Write(src)
node, err := html.Parse(&buf)
if err != nil {
return nil, nil, err
}
var (
f func(*html.Node) bool
body *html.Node
toc *tableofcontents.Fragments
toVisit []*html.Node
)
// find body
f = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.Data == "body" {
body = n
return true
}
if n.FirstChild != nil {
toVisit = append(toVisit, n.FirstChild)
}
if n.NextSibling != nil && f(n.NextSibling) {
return true
}
for len(toVisit) > 0 {
nv := toVisit[0]
toVisit = toVisit[1:]
if f(nv) {
return true
}
}
return false
}
if !f(node) {
return nil, nil, err
}
// remove by pandoc generated title
f = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.Data == "header" && attr(n, "id") == "title-block-header" {
n.Parent.RemoveChild(n)
return true
}
if n.FirstChild != nil {
toVisit = append(toVisit, n.FirstChild)
}
if n.NextSibling != nil && f(n.NextSibling) {
return true
}
for len(toVisit) > 0 {
nv := toVisit[0]
toVisit = toVisit[1:]
if f(nv) {
return true
}
}
return false
}
f(body)
// find toc
f = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.Data == "nav" && attr(n, "id") == "TOC" {
toc = parseTOC(n)
if !a.cfg.MarkupConfig().Pandoc.PreserveTOC {
n.Parent.RemoveChild(n)
}
return true
}
if n.FirstChild != nil {
toVisit = append(toVisit, n.FirstChild)
}
if n.NextSibling != nil && f(n.NextSibling) {
return true
}
for len(toVisit) > 0 {
nv := toVisit[0]
toVisit = toVisit[1:]
if f(nv) {
return true
}
}
return false
}
f(body)
if err != nil {
return nil, nil, err
}
buf.Reset()
err = html.Render(&buf, body)
if err != nil {
return nil, nil, err
}
// ltrim <html><head></head><body>\n\n and rtrim \n\n</body></html> which are added by html.Render
res := buf.Bytes()[8:]
res = res[:len(res)-9]
return res, toc, nil
}
// parseTOC returns a TOC root from the given toc Node
func parseTOC(doc *html.Node) *tableofcontents.Fragments {
var (
toc tableofcontents.Builder
f func(*html.Node, int, int)
)
f = func(n *html.Node, row, level int) {
if n.Type == html.ElementNode {
switch n.Data {
case "ul":
if level == 0 {
row++
}
level++
f(n.FirstChild, row, level)
case "li":
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type != html.ElementNode || c.Data != "a" {
continue
}
href := attr(c, "href")[1:]
toc.AddAt(&tableofcontents.Heading{
Title: nodeContent(c),
ID: href,
}, row, level)
}
f(n.FirstChild, row, level)
}
}
if n.NextSibling != nil {
f(n.NextSibling, row, level)
}
}
f(doc.FirstChild, -1, 0)
return toc.Build()
}
func attr(node *html.Node, key string) string {
for _, a := range node.Attr {
if a.Key == key {
return a.Val
}
}
return ""
}
func nodeContent(node *html.Node) string {
var buf bytes.Buffer
for c := node.FirstChild; c != nil; c = c.NextSibling {
html.Render(&buf, c)
}
return buf.String()
}
// Supports returns whether Pandoc is installed on this computer.
func Supports() bool {
hasBin := getPandocBinaryName() != ""

View file

@ -0,0 +1,85 @@
// Copyright 2021 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 pandoc_test
import (
"testing"
"github.com/gohugoio/hugo/hugolib"
)
func TestBasicConversion(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- content/p1.md --
testContent
-- layouts/_default/single.html --
{{ .Content }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
).Build()
b.AssertFileContent("public/p1/index.html", `<p>testContent</p>`)
}
func TestConversionWithHeader(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- content/p1.md --
# testContent
-- layouts/_default/single.html --
{{ .Content }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
).Build()
b.AssertFileContent("public/p1/index.html", `<h1 id="testcontent">testContent</h1>`)
}
func TestConversionWithExtractedToc(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- content/p1.md --
# title 1
## title 2
-- layouts/_default/single.html --
{{ .TableOfContents }}
{{ .Content }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
).Build()
b.AssertFileContent("public/p1/index.html", "<nav id=\"TableOfContents\">\n <ul>\n <li><a href=\"#title-2\">title 2</a></li>\n </ul>\n</nav>\n<h1 id=\"title-1\">title 1</h1>\n<h2 id=\"title-2\">title 2</h2>")
}

View file

@ -0,0 +1,27 @@
// Copyright 2020 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 pandocdoc_config holds pandoc related configuration.
package pandoc_config
var (
// Default holds Hugo's default pandoc configuration.
Default = Config{
PreserveTOC: false,
}
)
// Config configures pandoc.
type Config struct {
PreserveTOC bool
}

View file

@ -128,7 +128,7 @@ func (e *errorResource) Exif() *exif.ExifInfo {
panic(e.ResourceError)
}
func (e *errorResource) Colors() ([]string, error) {
func (e *errorResource) Colors() ([]images.Color, error) {
panic(e.ResourceError)
}

View file

@ -67,7 +67,7 @@ type imageResource struct {
meta *imageMeta
dominantColorInit sync.Once
dominantColors []string
dominantColors []images.Color
baseResource
}
@ -143,7 +143,7 @@ func (i *imageResource) getExif() *exif.ExifInfo {
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
func (i *imageResource) Colors() ([]string, error) {
func (i *imageResource) Colors() ([]images.Color, error) {
var err error
i.dominantColorInit.Do(func() {
var img image.Image
@ -153,7 +153,7 @@ func (i *imageResource) Colors() ([]string, error) {
}
colors := color_extractor.ExtractColors(img)
for _, c := range colors {
i.dominantColors = append(i.dominantColors, images.ColorToHexString(c))
i.dominantColors = append(i.dominantColors, images.ColorGoToColor(c))
}
})
return i.dominantColors, nil

View file

@ -85,9 +85,16 @@ func TestImageTransformBasic(t *testing.T) {
assertWidthHeight(c, img, w, h)
}
colors, err := image.Colors()
gotColors, err := image.Colors()
c.Assert(err, qt.IsNil)
c.Assert(colors, qt.DeepEquals, []string{"#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b"})
expectedColors := images.HexStringsToColors("#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b")
c.Assert(len(gotColors), qt.Equals, len(expectedColors))
for i := range gotColors {
c1, c2 := gotColors[i], expectedColors[i]
c.Assert(c1.ColorHex(), qt.Equals, c2.ColorHex())
c.Assert(c1.ColorGo(), qt.DeepEquals, c2.ColorGo())
c.Assert(c1.Luminance(), qt.Equals, c2.Luminance())
}
c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
c.Assert(image.ResourceType(), qt.Equals, "image")
@ -445,6 +452,24 @@ func TestImageExif(t *testing.T) {
getAndCheckExif(c, image)
}
func TestImageColorsLuminance(t *testing.T) {
c := qt.New(t)
_, image := fetchSunset(c)
c.Assert(image, qt.Not(qt.IsNil))
colors, err := image.Colors()
c.Assert(err, qt.IsNil)
c.Assert(len(colors), qt.Equals, 6)
var prevLuminance float64
for i, color := range colors {
luminance := color.Luminance()
c.Assert(err, qt.IsNil)
c.Assert(luminance > 0, qt.IsTrue)
c.Assert(luminance, qt.Not(qt.Equals), prevLuminance, qt.Commentf("i=%d", i))
prevLuminance = luminance
}
}
func BenchmarkImageExif(b *testing.B) {
getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource {
spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})

View file

@ -16,10 +16,76 @@ package images
import (
"encoding/hex"
"fmt"
"hash/fnv"
"image/color"
"math"
"strings"
"github.com/gohugoio/hugo/common/hstrings"
)
type colorGoProvider interface {
ColorGo() color.Color
}
type Color struct {
// The color.
color color.Color
// The color prefixed with a #.
hex string
// The relative luminance of the color.
luminance float64
}
// Luminance as defined by w3.org.
// See https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
func (c Color) Luminance() float64 {
return c.luminance
}
// ColorGo returns the color as a color.Color.
// For internal use only.
func (c Color) ColorGo() color.Color {
return c.color
}
// ColorHex returns the color as a hex string prefixed with a #.
func (c Color) ColorHex() string {
return c.hex
}
// String returns the color as a hex string prefixed with a #.
func (c Color) String() string {
return c.hex
}
// For hashstructure. This struct is used in template func options
// that needs to be able to hash a Color.
// For internal use only.
func (c Color) Hash() (uint64, error) {
h := fnv.New64a()
h.Write([]byte(c.hex))
return h.Sum64(), nil
}
func (c *Color) init() error {
c.hex = ColorGoToHexString(c.color)
r, g, b, _ := c.color.RGBA()
c.luminance = 0.2126*c.toSRGB(uint8(r)) + 0.7152*c.toSRGB(uint8(g)) + 0.0722*c.toSRGB(uint8(b))
return nil
}
func (c Color) toSRGB(i uint8) float64 {
v := float64(i) / 255
if v <= 0.04045 {
return v / 12.92
} else {
return math.Pow((v+0.055)/1.055, 2.4)
}
}
// AddColorToPalette adds c as the first color in p if not already there.
// Note that it does no additional checks, so callers must make sure
// that the palette is valid for the relevant format.
@ -45,14 +111,60 @@ func ReplaceColorInPalette(c color.Color, p color.Palette) {
p[p.Index(c)] = c
}
// ColorToHexString converts a color to a hex string.
func ColorToHexString(c color.Color) string {
// ColorGoToHexString converts a color.Color to a hex string.
func ColorGoToHexString(c color.Color) string {
r, g, b, a := c.RGBA()
rgba := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
if rgba.A == 0xff {
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
}
return fmt.Sprintf("#%.2x%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B, rgba.A)
}
func hexStringToColor(s string) (color.Color, error) {
// ColorGoToColor converts a color.Color to a Color.
func ColorGoToColor(c color.Color) Color {
cc := Color{color: c}
if err := cc.init(); err != nil {
panic(err)
}
return cc
}
func hexStringToColor(s string) Color {
c, err := hexStringToColorGo(s)
if err != nil {
panic(err)
}
return ColorGoToColor(c)
}
// HexStringsToColors converts a slice of hex strings to a slice of Colors.
func HexStringsToColors(s ...string) []Color {
var colors []Color
for _, v := range s {
colors = append(colors, hexStringToColor(v))
}
return colors
}
func toColorGo(v any) (color.Color, bool, error) {
switch vv := v.(type) {
case colorGoProvider:
return vv.ColorGo(), true, nil
default:
s, ok := hstrings.ToString(v)
if !ok {
return nil, false, nil
}
c, err := hexStringToColorGo(s)
if err != nil {
return nil, false, err
}
return c, true, nil
}
}
func hexStringToColorGo(s string) (color.Color, error) {
s = strings.TrimPrefix(s, "#")
if len(s) != 3 && len(s) != 4 && len(s) != 6 && len(s) != 8 {

View file

@ -46,7 +46,7 @@ func TestHexStringToColor(t *testing.T) {
c.Run(test.arg, func(c *qt.C) {
c.Parallel()
result, err := hexStringToColor(test.arg)
result, err := hexStringToColorGo(test.arg)
if b, ok := test.expect.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
@ -70,13 +70,18 @@ func TestColorToHexString(t *testing.T) {
{color.White, "#ffffff"},
{color.Black, "#000000"},
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}, "#4287f5"},
// 50% opacity.
// Note that the .Colors (dominant colors) received from the Image resource
// will always have an alpha value of 0xff.
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0x80}, "#4287f580"},
} {
test := test
c.Run(test.expect, func(c *qt.C) {
c.Parallel()
result := ColorToHexString(test.arg)
result := ColorGoToHexString(test.arg)
c.Assert(result, qt.Equals, test.expect)
})
@ -91,9 +96,9 @@ func TestAddColorToPalette(t *testing.T) {
c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
blue1, _ := hexStringToColor("34c3eb")
blue2, _ := hexStringToColor("34c3eb")
white, _ := hexStringToColor("fff")
blue1, _ := hexStringToColorGo("34c3eb")
blue2, _ := hexStringToColorGo("34c3eb")
white, _ := hexStringToColorGo("fff")
c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
@ -104,10 +109,18 @@ func TestReplaceColorInPalette(t *testing.T) {
c := qt.New(t)
palette := color.Palette{color.White, color.Black}
offWhite, _ := hexStringToColor("fcfcfc")
offWhite, _ := hexStringToColorGo("fcfcfc")
ReplaceColorInPalette(offWhite, palette)
c.Assert(palette, qt.HasLen, 2)
c.Assert(palette[0], qt.Equals, offWhite)
}
func TestColorLuminance(t *testing.T) {
c := qt.New(t)
c.Assert(hexStringToColor("#000000").Luminance(), qt.Equals, 0.0)
c.Assert(hexStringToColor("#768a9a").Luminance(), qt.Equals, 0.24361603589088263)
c.Assert(hexStringToColor("#d5bc9f").Luminance(), qt.Equals, 0.5261577672685374)
c.Assert(hexStringToColor("#ffffff").Luminance(), qt.Equals, 1.0)
}

View file

@ -171,7 +171,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
return i, nil, err
}
i.BgColor, err = hexStringToColor(i.Imaging.BgColor)
i.BgColor, err = hexStringToColorGo(i.Imaging.BgColor)
if err != nil {
return i, nil, err
}
@ -230,7 +230,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
c.Hint = hint
} else if part[0] == '#' {
c.BgColorStr = part[1:]
c.BgColor, err = hexStringToColor(c.BgColorStr)
c.BgColor, err = hexStringToColorGo(c.BgColorStr)
if err != nil {
return c, err
}
@ -424,7 +424,7 @@ type ImagingConfigInternal struct {
func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
var err error
i.BgColor, err = hexStringToColor(externalCfg.BgColor)
i.BgColor, err = hexStringToColorGo(externalCfg.BgColor)
if err != nil {
return err
}

View file

@ -132,7 +132,7 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
c.qualitySetForImage = quality != 75
c.Rotate = rotate
c.BgColorStr = bgColor
c.BgColor, _ = hexStringToColor(bgColor)
c.BgColor, _ = hexStringToColorGo(bgColor)
if filter != "" {
filter = strings.ToLower(filter)

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
@ -65,7 +65,7 @@ func (*Filters) Opacity(opacity any) gift.Filter {
func (*Filters) Text(text string, options ...any) gift.Filter {
tf := textFilter{
text: text,
color: "#ffffff",
color: color.White,
size: 20,
x: 10,
y: 10,
@ -78,7 +78,9 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
for option, v := range opt {
switch option {
case "color":
tf.color = cast.ToString(v)
if color, ok, _ := toColorGo(v); ok {
tf.color = color
}
case "size":
tf.size = cast.ToFloat64(v)
case "x":
@ -128,15 +130,14 @@ func (*Filters) Padding(args ...any) gift.Filter {
var top, right, bottom, left int
var ccolor color.Color = color.White // canvas color
var err error
_args := args // preserve original args for most stable hash
if vcs, ok := (args[len(args)-1]).(string); ok {
ccolor, err = hexStringToColor(vcs)
if vcs, ok, err := toColorGo(args[len(args)-1]); ok || err != nil {
if err != nil {
panic("invalid canvas color: specify RGB or RGBA using hex notation")
}
ccolor = vcs
args = args[:len(args)-1]
if len(args) == 0 {
panic("not enough arguments: provide one or more padding values using the CSS shorthand property syntax")
@ -180,12 +181,11 @@ func (*Filters) Padding(args ...any) gift.Filter {
// Dither creates a filter that dithers an image.
func (*Filters) Dither(options ...any) gift.Filter {
ditherOptions := struct {
Colors []string
Colors []any
Method string
Serpentine bool
Strength float32
}{
Colors: []string{"000000ff", "ffffffff"},
Method: "floydsteinberg",
Serpentine: true,
Strength: 1.0,
@ -198,14 +198,18 @@ func (*Filters) Dither(options ...any) gift.Filter {
}
}
if len(ditherOptions.Colors) == 0 {
ditherOptions.Colors = []any{"000000ff", "ffffffff"}
}
if len(ditherOptions.Colors) < 2 {
panic("palette must have at least two colors")
}
var palette []color.Color
for _, c := range ditherOptions.Colors {
cc, err := hexStringToColor(c)
if err != nil {
cc, ok, err := toColorGo(c)
if !ok || err != nil {
panic(fmt.Sprintf("%q is an invalid color: specify RGB or RGBA using hexadecimal notation", c))
}
palette = append(palette, cc)

View file

@ -63,7 +63,7 @@ type ImageResourceOps interface {
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
Colors() ([]string, error)
Colors() ([]Color, error)
// For internal use.
DecodeImage() (image.Image, error)

View file

@ -15,6 +15,7 @@ package images
import (
"image"
"image/color"
"image/draw"
"io"
"strings"
@ -31,7 +32,8 @@ import (
var _ gift.Filter = (*textFilter)(nil)
type textFilter struct {
text, color string
text string
color color.Color
x, y int
size float64
linespacing int
@ -39,11 +41,6 @@ type textFilter struct {
}
func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
color, err := hexStringToColor(f.color)
if err != nil {
panic(err)
}
// Load and parse font
ttf := goregular.TTF
if f.fontSource != nil {
@ -74,7 +71,7 @@ func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options)
d := font.Drawer{
Dst: dst,
Src: image.NewUniform(color),
Src: image.NewUniform(f.color),
Face: face,
}

View file

@ -263,7 +263,7 @@ func (r *resourceAdapter) Exif() *exif.ExifInfo {
return r.getImageOps().Exif()
}
func (r *resourceAdapter) Colors() ([]string, error) {
func (r *resourceAdapter) Colors() ([]images.Color, error) {
return r.getImageOps().Colors()
}

View file

@ -22,6 +22,7 @@ Renders an embedded YouTube video.
*/}}
{{- $pc := .Page.Site.Config.Privacy.YouTube }}
{{- $remoteErrID := "err-youtube-remote" }}
{{- if not $pc.Disable }}
{{- with $id := or (.Get "id") (.Get 0) }}
@ -31,12 +32,12 @@ Renders an embedded YouTube video.
{{- $data := dict }}
{{- with resources.GetRemote $url }}
{{- with .Err }}
{{- errorf "The %q shortcode was unable to get remote resource %q. %s. See %s" $.Name $url . $.Position }}
{{- erroridf $remoteErrID "The %q shortcode was unable to get remote resource %q. %s. See %s" $.Name $url . $.Position }}
{{- else }}
{{- $data = .Content | transform.Unmarshal }}
{{- end }}
{{- else }}
{{- errorf "The %q shortcode was unable to get remote resource %q. See %s" $.Name $url $.Position }}
{{- erroridf $remoteErrID "The %q shortcode was unable to get remote resource %q. See %s" $.Name $url $.Position }}
{{- end }}
{{/* Set defaults. */}}