Files
Olivier Meunier 791174eefa Improved image "content negotiation"
Currently, there's no real content negotiation for images in srcset or
source tags, only something based on the optional type attribute.

Part of the conversion process consists of converting picture elements
to img with an srcset attribute.

Before archival, every img[srcset] attribute is reconstructed
in order to sort the list and have the biggest candidates first (and
discard images that will be way too big)

The archiver will then try to download every image in the given order
and, once successful, set an src attribute to the image (with the winner)
and remove the srcset.

Finally, this adds 2 new steps in the CleanDomProcessor:

- remove empty id attributes
- remove img with no src and not srcset attribute
2025-07-04 23:14:57 +02:00

248 lines
5.0 KiB
Go

// SPDX-FileCopyrightText: © 2017 Mads Jacobsen
//
// SPDX-License-Identifier: MIT
// Original code: https://github.com/lukasbob/srcset
// Package srcset is an srcset value parser.
package srcset
import (
"regexp"
"strconv"
"strings"
)
// ImageSource is a structure that contains an image definition.
type ImageSource struct {
URL string
Width int64
Height int64
Density float64
}
// SourceSet is the result of parsing the value of a srcset attribute.
// A SourceSet consists of multiple ImageSource instances.
type SourceSet []ImageSource
const (
comma = ','
leftParens = '('
rightParens = ')'
)
const (
stateNone = iota
stateInDescriptor
stateInParens
stateAfterDescriptor
)
var (
regexLeadingSpaces = regexp.MustCompile("^[ \t\n\r\u000c]+")
regexLeadingCommasOrSpaces = regexp.MustCompile("^[, \t\n\r\u000c]+")
regexLeadingNotSpaces = regexp.MustCompile("^[^ \t\n\r\u000c]+")
regexTrailingCommas = regexp.MustCompile("[,]+$")
regexNonNegativeInteger = regexp.MustCompile(`^\d+$`)
regexFloatingPoint = regexp.MustCompile(`^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$`)
)
func isSpace(c rune) bool {
switch c {
case
'\u0020', // space
'\u0009', // horizontal tab
'\u000A', // new line
'\u000C', // form feed
'\u000D': // carriage return
return true
default:
return false
}
}
// Parse takes the value of a srcset attribute and parses it.
//
//nolint:gocognit,gocyclo
func Parse(input string) SourceSet {
var (
url string
pos = 0
currState = stateNone
end = len(input)
candidates = SourceSet{}
descriptors = []string{}
)
collectChars := func(rx *regexp.Regexp) string {
if match := rx.FindString(input[pos:]); match != "" {
pos += len(match)
return match
}
return ""
}
parseDescriptors := func() {
var (
isErr = false
h int64
w int64
d float64
)
for _, desc := range descriptors {
lastIdx := len(desc) - 1
lastChar, numericVal := desc[lastIdx], desc[:lastIdx]
intVal, intErr := strconv.ParseInt(numericVal, 10, 64)
floatVal, floatErr := strconv.ParseFloat(numericVal, 64)
switch {
case regexNonNegativeInteger.MatchString(numericVal) && lastChar == 'w':
if w != 0 || d != 0 {
isErr = true
}
if intErr != nil || intVal == 0 {
isErr = true
} else {
w = intVal
}
case regexFloatingPoint.MatchString(numericVal) && lastChar == 'x':
if w != 0 || d != 0 || h != 0 {
isErr = true
}
if floatErr != nil || floatVal < 0 {
isErr = true
} else {
d = floatVal
}
case regexNonNegativeInteger.MatchString(numericVal) && lastChar == 'h':
if h != 0 || d != 0 {
isErr = true
}
if intErr != nil || intVal == 0 {
isErr = true
} else {
h = intVal
}
default:
isErr = true
}
}
if !isErr {
candidates = append(candidates, ImageSource{
URL: url,
Density: d,
Width: w,
Height: h,
})
}
}
tokenize := func() {
collectChars(regexLeadingSpaces)
currDescriptor := ""
currState = stateInDescriptor
for {
if pos == len(input) {
if currState != stateAfterDescriptor && currDescriptor != "" {
descriptors = append(descriptors, currDescriptor)
}
parseDescriptors()
return
}
c := rune(input[pos])
switch currState {
case stateInDescriptor:
switch {
case isSpace(c):
if currDescriptor != "" {
descriptors = append(descriptors, currDescriptor)
currDescriptor = ""
currState = stateAfterDescriptor
}
case c == comma:
pos++
if currDescriptor != "" {
descriptors = append(descriptors, currDescriptor)
parseDescriptors()
return
}
case c == leftParens:
currDescriptor += string(c)
currState = stateInParens
default:
currDescriptor += string(c)
}
case stateInParens:
switch c {
case rightParens:
currDescriptor += string(c)
currState = stateInDescriptor
default:
currDescriptor += string(c)
}
case stateAfterDescriptor:
switch {
case isSpace(c):
default:
currState = stateInDescriptor
pos--
}
}
pos++
}
}
for {
collectChars(regexLeadingCommasOrSpaces)
if pos >= end {
return candidates
}
url = collectChars(regexLeadingNotSpaces)
descriptors = []string{}
if url[len(url)-1] == ',' {
url = regexTrailingCommas.ReplaceAllString(url, "")
parseDescriptors()
} else {
tokenize()
}
}
}
func (s ImageSource) String() string {
b := new(strings.Builder)
b.WriteString(s.URL)
if s.Width > 0 {
b.WriteString(" " + strconv.FormatInt(s.Width, 10) + "w")
}
if s.Height > 0 {
b.WriteString(" " + strconv.FormatInt(s.Height, 10) + "h")
}
if s.Density > 0 {
b.WriteString(" " + strconv.FormatFloat(s.Density, 'f', -1, 64) + "x")
}
return b.String()
}
func (s SourceSet) String() string {
res := make([]string, len(s))
for i, p := range s {
res[i] = p.String()
}
return strings.Join(res, ", ")
}