152 lines
4.3 KiB
Go
152 lines
4.3 KiB
Go
package site
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"image"
|
|
_ "image/jpeg" // DecodeConfig for the sizes of pictures in media/
|
|
_ "image/png"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Pictures in static/media/ are published as the editor writes them: a main
|
|
// file (dock.jpg), smaller sizes beside it (dock-480w.jpg, dock-960w.jpg) and
|
|
// WebP copies of each (dock.webp, dock-480w.webp). A Markdown image that
|
|
// points at the main file is published as a <picture> offering all of them,
|
|
// with its width and height so the page doesn't jump while it loads.
|
|
|
|
// MediaPicture is one picture in media/ and the sizes it's published at.
|
|
type MediaPicture struct {
|
|
Width, Height int
|
|
Sizes []MediaSize // narrowest first
|
|
}
|
|
|
|
// MediaSize is one published file of a picture.
|
|
type MediaSize struct {
|
|
URL string
|
|
Width int
|
|
WebP bool
|
|
}
|
|
|
|
var mediaVariantRe = regexp.MustCompile(`^(.+)-(\d+)w\.(jpg|png|webp)$`)
|
|
|
|
// LoadMedia indexes static/media/, by the main file's address.
|
|
func LoadMedia(staticDir string) map[string]*MediaPicture {
|
|
root := filepath.Join(staticDir, "media")
|
|
type found struct {
|
|
main string // its address, if the main JPEG or PNG is there
|
|
mainWebP bool
|
|
sizes []MediaSize
|
|
}
|
|
byStem := map[string]*found{}
|
|
get := func(stem string) *found {
|
|
if byStem[stem] == nil {
|
|
byStem[stem] = &found{}
|
|
}
|
|
return byStem[stem]
|
|
}
|
|
_ = filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
rel, _ := filepath.Rel(staticDir, p)
|
|
u := "/" + filepath.ToSlash(rel)
|
|
dir, name := path.Split(u)
|
|
if m := mediaVariantRe.FindStringSubmatch(name); m != nil {
|
|
w, _ := strconv.Atoi(m[2])
|
|
get(dir + m[1]).sizes = append(get(dir+m[1]).sizes, MediaSize{URL: u, Width: w, WebP: m[3] == "webp"})
|
|
return nil
|
|
}
|
|
ext := path.Ext(name)
|
|
stem := dir + strings.TrimSuffix(name, ext)
|
|
switch strings.ToLower(ext) {
|
|
case ".jpg", ".jpeg", ".png":
|
|
get(stem).main = u
|
|
case ".webp":
|
|
get(stem).mainWebP = true
|
|
}
|
|
return nil
|
|
})
|
|
out := map[string]*MediaPicture{}
|
|
for stem, f := range byStem {
|
|
if f.main == "" {
|
|
continue
|
|
}
|
|
file, err := os.Open(filepath.Join(staticDir, filepath.FromSlash(f.main)))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
cfg, _, err := image.DecodeConfig(file)
|
|
file.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pic := &MediaPicture{Width: cfg.Width, Height: cfg.Height}
|
|
mainExt := path.Ext(f.main)
|
|
for _, sz := range f.sizes {
|
|
// Only sizes of this main file's kind, or its WebP copies.
|
|
if sz.Width < cfg.Width && (sz.WebP || path.Ext(sz.URL) == mainExt) {
|
|
pic.Sizes = append(pic.Sizes, sz)
|
|
}
|
|
}
|
|
pic.Sizes = append(pic.Sizes, MediaSize{URL: f.main, Width: cfg.Width})
|
|
if f.mainWebP {
|
|
pic.Sizes = append(pic.Sizes, MediaSize{URL: stem + ".webp", Width: cfg.Width, WebP: true})
|
|
}
|
|
sort.SliceStable(pic.Sizes, func(i, j int) bool { return pic.Sizes[i].Width < pic.Sizes[j].Width })
|
|
out[f.main] = pic
|
|
}
|
|
return out
|
|
}
|
|
|
|
const defaultImageSizes = "(min-width: 50rem) 50rem, 100vw"
|
|
|
|
var imgTagRe = regexp.MustCompile(`<img src="(/media/[^"]+)"([^>]*?)\s*/?>`)
|
|
|
|
// pictures turns <img> tags pointing at media/ into responsive pictures.
|
|
func (m *Markdown) pictures(out []byte) []byte {
|
|
if len(m.media) == 0 {
|
|
return out
|
|
}
|
|
return imgTagRe.ReplaceAllFunc(out, func(tag []byte) []byte {
|
|
sub := imgTagRe.FindSubmatch(tag)
|
|
src := html.UnescapeString(string(sub[1]))
|
|
pic := m.media[src]
|
|
if pic == nil {
|
|
return tag
|
|
}
|
|
rest := string(sub[2])
|
|
var plain, webp []string
|
|
for _, sz := range pic.Sizes {
|
|
entry := fmt.Sprintf("%s %dw", html.EscapeString(sz.URL), sz.Width)
|
|
if sz.WebP {
|
|
webp = append(webp, entry)
|
|
} else {
|
|
plain = append(plain, entry)
|
|
}
|
|
}
|
|
sizes := html.EscapeString(m.imageSizes)
|
|
var b strings.Builder
|
|
img := fmt.Sprintf(`<img src="%s"%s width="%d" height="%d"`, html.EscapeString(src), rest, pic.Width, pic.Height)
|
|
if len(plain) > 1 {
|
|
img += fmt.Sprintf(` srcset="%s" sizes="%s"`, strings.Join(plain, ", "), sizes)
|
|
}
|
|
if !strings.Contains(rest, "loading=") {
|
|
img += ` loading="lazy" decoding="async"`
|
|
}
|
|
img += ">"
|
|
if len(webp) == 0 {
|
|
return []byte(img)
|
|
}
|
|
fmt.Fprintf(&b, `<picture><source type="image/webp" srcset="%s" sizes="%s">%s</picture>`, strings.Join(webp, ", "), sizes, img)
|
|
return []byte(b.String())
|
|
})
|
|
}
|