Skip to content

Commit bbd9526

Browse files
committed
chore: improve assets hook, add image resize
1 parent 606fde6 commit bbd9526

14 files changed

Lines changed: 826 additions & 193 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ assets(enabled), encrypt(enabled), shortcode(enabled), filter, minify, pelican,
124124
125125
## 文档
126126
127-
完整文档参照 [docs/](docs/) 目录,或在线访问 [https://snow.honmaple.com](https://snow.honmaple.me)。
127+
完整文档参照 [docs/](docs/) 目录,或在线访问 [https://docs.honmaple.com/snow/](https://docs.honmaple.com/snow)。
128128
129129
## License
130130

docs/content/hooks/assets.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ weight: 20
44
---
55
## Assets
66

7-
静态资源处理:SCSS 编译、合并、压缩。
7+
静态资源处理:SCSS 编译、合并、压缩、图片缩放
88

99
```yaml
1010
hooks:
@@ -26,18 +26,32 @@ hooks:
2626
filters:
2727
- "jsmin"
2828
output: "static/script.min.js"
29+
cover:
30+
files:
31+
- "images/cover.jpg"
32+
filters:
33+
- name: "image"
34+
width: 1200
35+
height: 800
36+
fit: "inside"
37+
quality: 82
38+
output: "static/cover.jpg"
2939
```
3040
3141
| 选项 | 说明 |
3242
|------|------|
3343
| `files` | 源文件,必填,支持 glob |
3444
| `sass_compiler` | Sass 编译器,可选值:`libscss`、`dartsass`;只影响 `.scss` / `.sass` 文件 |
35-
| `filters` | 合并后的过滤器,可选值:`cssmin`、`jsmin` |
45+
| `filters` | 过滤器,可使用字符串列表或对象步骤;可选值:`cssmin`、`jsmin`、`image` |
3646
| `output` | 输出路径,必填 |
3747

3848
`.scss`/`.sass` 文件自动编译为 CSS。默认使用 `libscss`;配置 `sass_compiler: "dartsass"` 时改用 Dart Sass Embedded,需要本机可执行文件 `sass` 在 `PATH` 中可用,并支持 `sass --embedded`。
3949

40-
配置校验会在插件初始化时执行:缺少 `files`、缺少 `output` 或使用未知 `filters` 都会使构建失败。
50+
`cssmin`、`jsmin` 用于合并后的文本资源处理,不接受参数。`image` 用于单个图片文件,支持 `width`、`height`、`fit`、`quality` 参数;`fit` 可选 `inside`、`cover`、`fill`,默认 `inside`;`quality` 仅用于 JPEG,范围 `1..100`,默认 `85`。图片输出格式由 `output` 扩展名决定,支持 `.jpg`、`.jpeg`、`.png`、`.gif`;GIF 会按静态图处理,不保留动画。
51+
52+
使用 `image` filter 时,`files` 的 glob 最终必须只匹配一个图片文件,不会执行文本合并。模板内联 `assets` 标签只支持字符串形式的 `filters`,带参数的图片处理建议写在配置中。
53+
54+
配置校验会在插件初始化时执行:缺少 `files`、缺少 `output`、使用未知 `filters` 或非法 filter 参数都会使构建失败。
4155

4256
模板:
4357

docs/content/templates/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Date 格式参考:
166166
{% endassets %}
167167
```
168168

169-
需在配置中启用 `hooks.assets``sass_compiler` 可使用 `libscss``dartsass`,只影响 `.scss` / `.sass` 文件;`filters` 可使用 `cssmin``jsmin`,用于合并后的处理。`dartsass` 需要本机可执行文件 `sass``PATH` 中可用,并支持 `sass --embedded`
169+
需在配置中启用 `hooks.assets``sass_compiler` 可使用 `libscss``dartsass`,只影响 `.scss` / `.sass` 文件;模板内联 `filters` 支持字符串形式的 `cssmin``jsmin`,用于合并后的处理。带参数的 `image` filter 建议写在 `hooks.assets.option` 配置中`dartsass` 需要本机可执行文件 `sass``PATH` 中可用,并支持 `sass --embedded`
170170

171171
## Shortcode
172172

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/bep/godartsass/v2 v2.5.0
99
github.com/bep/golibsass v1.1.1
1010
github.com/bmatcuk/doublestar/v4 v4.10.0
11+
github.com/disintegration/imaging v1.6.2
1112
github.com/expr-lang/expr v1.17.8
1213
github.com/flosch/pongo2/v7 v7.0.0-alpha.1
1314
github.com/fsnotify/fsnotify v1.10.1
@@ -45,6 +46,7 @@ require (
4546
github.com/subosito/gotenv v1.4.1 // indirect
4647
github.com/tdewolff/parse/v2 v2.6.4 // indirect
4748
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
49+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
4850
golang.org/x/sys v0.44.0 // indirect
4951
golang.org/x/text v0.33.0 // indirect
5052
google.golang.org/protobuf v1.35.2 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
6666
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6767
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6868
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
69+
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
70+
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
6971
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
7072
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
7173
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
@@ -264,6 +266,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
264266
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
265267
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
266268
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
269+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
270+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
267271
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
268272
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
269273
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

internal/site/content/node.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ type (
1212
File *File
1313
FrontMatter *FrontMatter
1414

15-
Toc []*Heading
16-
Lang string
17-
Slug string
18-
Title string
19-
Summary string
20-
Content string
21-
RawContent string
15+
Toc []*Heading
16+
Lang string
17+
Slug string
18+
Title string
19+
Summary string
20+
Content string
21+
RawContent string
22+
Description string
2223
}
2324
Heading = parser.Heading
2425
)
@@ -65,6 +66,7 @@ func (d *Processor) parseNode(fullpath string) (*Node, error) {
6566
Lang: lang,
6667
Slug: fm.GetString("slug"),
6768
Title: fm.GetString("title"),
69+
Description: fm.GetString("description"),
6870
Content: result.Content,
6971
Summary: result.Summary,
7072
Toc: result.Toc,

internal/site/hook/assets/asset.go

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,80 +7,80 @@ import (
77
stdpath "path"
88

99
"github.com/honmaple/snow/internal/core"
10-
"github.com/tdewolff/minify/v2"
11-
"github.com/tdewolff/minify/v2/css"
12-
"github.com/tdewolff/minify/v2/js"
1310
)
1411

15-
type Asset struct {
16-
Files []string
17-
Filters []string
18-
SassCompiler string
19-
Output string
20-
ShowVersion bool
21-
}
22-
23-
func (n *Asset) filter(name string, buf []byte) (result []byte, err error) {
24-
switch name {
25-
case "cssmin":
26-
result, err = n.cssmin(buf)
27-
case "jsmin":
28-
result, err = n.jsmin(buf)
12+
type (
13+
Asset struct {
14+
Files []string
15+
Filters []Filter
16+
SassCompiler SassCompiler
17+
Output string
18+
ShowVersion bool
2919
}
30-
return result, err
31-
}
32-
33-
func (n *Asset) cssmin(buf []byte) ([]byte, error) {
34-
m := minify.New()
35-
m.AddFunc("css", css.Minify)
20+
Filter interface {
21+
Name() string
22+
Execute([]byte) ([]byte, error)
23+
}
24+
)
3625

37-
return m.Bytes("css", buf)
26+
func (n *Asset) matchedFiles(assetsFS fs.FS) ([]string, error) {
27+
var results []string
28+
for _, file := range n.Files {
29+
matchedFiles, err := fs.Glob(assetsFS, file)
30+
if err != nil {
31+
return nil, err
32+
}
33+
results = append(results, matchedFiles...)
34+
}
35+
return results, nil
3836
}
3937

40-
func (n *Asset) jsmin(buf []byte) ([]byte, error) {
41-
m := minify.New()
42-
m.AddFunc("js", js.Minify)
43-
44-
return m.Bytes("js", buf)
38+
func (n *Asset) compileSass(assetsFS fs.FS, file string, buf []byte) ([]byte, error) {
39+
return n.SassCompiler.Compile(assetsFS, file, buf)
4540
}
4641

4742
func (n *Asset) Execute(ctx context.Context, assetsFS fs.FS, writer core.Writer) error {
43+
if n.hasImageFilter() {
44+
buf, err := n.executeImage(assetsFS)
45+
if err != nil {
46+
return err
47+
}
48+
return writer.WriteFile(ctx, n.Output, bytes.NewReader(buf))
49+
}
50+
4851
var (
4952
b bytes.Buffer
5053
)
5154
// 先合并再压缩
52-
for _, file := range n.Files {
53-
matchedFiles, err := fs.Glob(assetsFS, file)
55+
matchedFiles, err := n.matchedFiles(assetsFS)
56+
if err != nil {
57+
return err
58+
}
59+
for _, matchedFile := range matchedFiles {
60+
buf, err := fs.ReadFile(assetsFS, matchedFile)
5461
if err != nil {
5562
return err
5663
}
57-
58-
for _, matchedFile := range matchedFiles {
59-
buf, err := fs.ReadFile(assetsFS, matchedFile)
64+
switch stdpath.Ext(matchedFile) {
65+
case ".scss", ".sass":
66+
result, err := n.compileSass(assetsFS, matchedFile, buf)
6067
if err != nil {
6168
return err
6269
}
63-
switch stdpath.Ext(matchedFile) {
64-
case ".scss", ".sass":
65-
result, err := n.compileSass(assetsFS, matchedFile, buf)
66-
if err != nil {
67-
return err
68-
}
69-
b.Write(result)
70-
b.WriteString("\n")
71-
case ".css":
72-
b.Write(buf)
73-
b.WriteString("\n")
74-
case ".js":
75-
b.Write(buf)
76-
b.WriteString("\n;\n")
77-
}
70+
b.Write(result)
71+
b.WriteString("\n")
72+
case ".css":
73+
b.Write(buf)
74+
b.WriteString("\n")
75+
case ".js":
76+
b.Write(buf)
77+
b.WriteString("\n;\n")
7878
}
7979
}
8080

8181
buf := b.Bytes()
82-
for _, name := range n.Filters {
83-
result, err := n.filter(name, buf)
82+
for _, filter := range n.Filters {
83+
result, err := filter.Execute(buf)
8484
if err != nil {
8585
return err
8686
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package assets
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/spf13/cast"
9+
)
10+
11+
func normalizeFilters(value any) ([]Filter, error) {
12+
if value == nil {
13+
return nil, nil
14+
}
15+
16+
var names []string
17+
18+
switch filters := value.(type) {
19+
case string:
20+
names = strings.Split(filters, ",")
21+
case []string:
22+
names = filters
23+
case []any:
24+
results := make([]Filter, 0, len(filters))
25+
for _, filter := range filters {
26+
result, err := normalizeFilter(filter)
27+
if err != nil {
28+
return nil, err
29+
}
30+
if result != nil {
31+
results = append(results, result)
32+
}
33+
}
34+
return results, nil
35+
default:
36+
if reflect.TypeOf(value).Kind() == reflect.Slice {
37+
values := reflect.ValueOf(value)
38+
results := make([]Filter, 0, values.Len())
39+
for i := 0; i < values.Len(); i++ {
40+
result, err := normalizeFilter(values.Index(i).Interface())
41+
if err != nil {
42+
return nil, err
43+
}
44+
if result != nil {
45+
results = append(results, result)
46+
}
47+
}
48+
return results, nil
49+
}
50+
return nil, fmt.Errorf("filters must be a string or list")
51+
}
52+
53+
results := make([]Filter, 0, len(names))
54+
for _, name := range names {
55+
result, err := normalizeFilter(name)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if result != nil {
60+
results = append(results, result)
61+
}
62+
}
63+
return results, nil
64+
}
65+
66+
func normalizeFilter(value any) (Filter, error) {
67+
switch filter := value.(type) {
68+
case string:
69+
name := strings.TrimSpace(filter)
70+
if name == "" {
71+
return nil, nil
72+
}
73+
return newFilter(name, nil)
74+
case map[string]any:
75+
return normalizeFilterMap(filter)
76+
case map[any]any:
77+
options := make(map[string]any, len(filter))
78+
for key, value := range filter {
79+
options[cast.ToString(key)] = value
80+
}
81+
return normalizeFilterMap(options)
82+
default:
83+
return nil, fmt.Errorf("assets filter must be a string or object")
84+
}
85+
}
86+
87+
func normalizeFilterMap(options map[string]any) (Filter, error) {
88+
name := strings.TrimSpace(cast.ToString(options["name"]))
89+
if name == "" {
90+
return nil, fmt.Errorf("assets filter object requires name")
91+
}
92+
93+
filterOptions := make(map[string]any, len(options))
94+
for key, value := range options {
95+
if key == "name" {
96+
continue
97+
}
98+
filterOptions[key] = value
99+
}
100+
return newFilter(name, filterOptions)
101+
}
102+
103+
func newFilter(name string, options map[string]any) (Filter, error) {
104+
switch name {
105+
case assetFilterCSSMin, assetFilterJSMin:
106+
if len(options) > 0 {
107+
return nil, fmt.Errorf("assets filter %q does not accept options", name)
108+
}
109+
return &MinifyFilter{name: name}, nil
110+
case assetFilterImage:
111+
return newImageFilter(options)
112+
default:
113+
return nil, fmt.Errorf("unknown assets filter %q", name)
114+
}
115+
}

0 commit comments

Comments
 (0)