mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-14 15:12:15 +09:00
Backport #39708 Co-authored-by: Basavaraj S m <basavaraj@digiscrypt.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
Basavaraj S m
wxiaoguang
parent
f5fd936cbb
commit
63669003eb
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SrcSetCandidate struct {
|
||||
SrcLink string
|
||||
SrcDesc []string
|
||||
}
|
||||
|
||||
const (
|
||||
asciiSpaces = " \t\n\r\f"
|
||||
asciiSpacesAndComma = " \t\n\r\f,"
|
||||
)
|
||||
|
||||
func ParseSrcset(srcset string) (candidates []SrcSetCandidate) {
|
||||
// ref: https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
|
||||
// It can't simply use "split" because comma can appear in URLs and descriptors can be parenthesized and contain commas.
|
||||
extractLink := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpacesAndComma)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
end := strings.IndexAny(srcset, asciiSpaces)
|
||||
if end == -1 {
|
||||
end = len(srcset)
|
||||
}
|
||||
link := srcset[:end]
|
||||
srcset = srcset[end:]
|
||||
if strings.HasSuffix(link, ",") {
|
||||
return strings.TrimRight(link, ","), false
|
||||
}
|
||||
return link, true
|
||||
}
|
||||
extractDesc := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpaces)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
inParentheses := false
|
||||
for i := range len(srcset) {
|
||||
c := srcset[i]
|
||||
if inParentheses {
|
||||
inParentheses = c != ')'
|
||||
continue
|
||||
}
|
||||
if c == '(' {
|
||||
inParentheses = true
|
||||
continue
|
||||
}
|
||||
if strings.IndexByte(asciiSpacesAndComma, c) != -1 {
|
||||
desc := srcset[:i]
|
||||
srcset = srcset[i+1:]
|
||||
return desc, c != ','
|
||||
}
|
||||
}
|
||||
desc := srcset
|
||||
srcset = ""
|
||||
return desc, false
|
||||
}
|
||||
|
||||
for {
|
||||
link, hasDesc := extractLink()
|
||||
if link == "" {
|
||||
break
|
||||
}
|
||||
var descriptors []string
|
||||
if hasDesc {
|
||||
for {
|
||||
desc, hasMoreDesc := extractDesc()
|
||||
if desc != "" {
|
||||
descriptors = append(descriptors, desc)
|
||||
}
|
||||
if !hasMoreDesc {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, SrcSetCandidate{SrcLink: link, SrcDesc: descriptors})
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func EncodeSrcset(candidates []SrcSetCandidate) string {
|
||||
var sb strings.Builder
|
||||
for i, c := range candidates {
|
||||
if i != 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
link := c.SrcLink
|
||||
if prefix, ok := strings.CutSuffix(link, ","); ok {
|
||||
link = prefix + "%2C"
|
||||
}
|
||||
sb.WriteString(link)
|
||||
for _, d := range c.SrcDesc {
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(d)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSrcset(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input string
|
||||
candidates []SrcSetCandidate
|
||||
encoded string
|
||||
}{
|
||||
{
|
||||
name: "bare URL",
|
||||
input: "image.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "image.png"}},
|
||||
},
|
||||
{
|
||||
name: "bare URLs",
|
||||
input: "a.png, b.png , c.png,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png"}, {SrcLink: "c.png"}},
|
||||
encoded: "a.png, b.png, c.png",
|
||||
},
|
||||
{
|
||||
name: "empty candidates",
|
||||
input: ", ,\ta.png 1x,, ,b.png 2x, ,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png 1x, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "trailing commas in URL",
|
||||
input: "a.png,, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "comma in URL",
|
||||
input: "comma,in,url.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "comma,in,url.png"}},
|
||||
},
|
||||
{
|
||||
name: "embedded data URL",
|
||||
input: "data:image/png;base64,abcd 1x, image.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "data:image/png;base64,abcd", SrcDesc: []string{"1x"}}, {SrcLink: "image.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
{
|
||||
name: "parenthesized descriptor",
|
||||
input: "a.png (x,y) 1x, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"(x,y)", "1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
candidates := ParseSrcset(test.input)
|
||||
assert.Equal(t, test.candidates, candidates)
|
||||
encoded := EncodeSrcset(candidates)
|
||||
assert.Equal(t, util.IfZero(test.encoded, test.input), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("encode trailing commas", func(t *testing.T) {
|
||||
encoded := EncodeSrcset([]SrcSetCandidate{
|
||||
{SrcLink: "image,"},
|
||||
{SrcLink: "image,,", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light,", SrcDesc: []string{"3x"}},
|
||||
})
|
||||
assert.Equal(t, "image%2C, image,%2C 2x, image.svg?variant=light%2C 3x", encoded)
|
||||
assert.Equal(t, []SrcSetCandidate{
|
||||
{SrcLink: "image%2C"},
|
||||
{SrcLink: "image,%2C", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light%2C", SrcDesc: []string{"3x"}},
|
||||
}, ParseSrcset(encoded))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user