Files
sm-journal-plugin/classes/image_alt.go
Jakub Zych 0e739c882e fix: H1 conversion leaves markdown and unfinished tags alone
ConvertContentH1 rewrites content only when it is HTML; a markdown
source, its code samples included, is kept byte for byte and only its
rendered content_html is converted, on API saves, imports,
journal:fix-headings and translations. ConvertH1 and SetImageAlt keep
the bytes of an unfinished tag at the end of the input.
2026-11-04 23:14:42 +01:00

97 lines
2.4 KiB
Go

package classes
import (
"bytes"
"strings"
xhtml "golang.org/x/net/html"
)
// SetImageAlt sets the alt attribute of every img in html whose src
// attribute (unescaped) equals src, adding it when missing. It returns the
// new HTML, the number of matching images and the alt the first match had
// (nil when it had none). It works on the x/net/html tokenizer: a matching
// img tag is written again from its token, so every attribute value,
// alt included, is escaped by the tokenizer and never concatenated; every
// other byte of html is copied as it is, the bytes of an unfinished tag at
// the end included. An empty alt marks a decorative image (alt="").
func SetImageAlt(html, src, alt string) (string, int, *string) {
if src == "" || !containsFold(html, "<img") {
return html, 0, nil
}
z := xhtml.NewTokenizer(strings.NewReader(html))
var b bytes.Buffer
b.Grow(len(html) + len(alt))
matches := 0
var old *string
consumed := 0
for {
tt := z.Next()
if tt == xhtml.ErrorToken {
// The bytes of an unfinished tag at the end are kept.
b.WriteString(html[min(consumed, len(html)):])
break
}
raw := z.Raw()
consumed += len(raw)
if tt == xhtml.StartTagToken || tt == xhtml.SelfClosingTagToken {
// Raw is copied before Token, which reads the tag; Token is
// the only reader of the tag (TagName would consume it).
kept := append([]byte(nil), raw...)
tok := z.Token()
if tok.Data == "img" && imgSrc(tok) == src {
prev := setAlt(&tok, alt)
if matches == 0 {
old = prev
}
matches++
b.WriteString(tok.String())
continue
}
b.Write(kept)
continue
}
b.Write(raw)
}
if matches == 0 {
return html, 0, nil
}
return b.String(), matches, old
}
func imgSrc(tok xhtml.Token) string {
for _, a := range tok.Attr {
if a.Namespace == "" && a.Key == "src" {
return a.Val
}
}
return ""
}
// setAlt sets tok's alt to alt and returns the previous value (nil when
// there was none). Repeated alt attributes collapse to one.
func setAlt(tok *xhtml.Token, alt string) *string {
var prev *string
attrs := tok.Attr[:0]
set := false
for _, a := range tok.Attr {
if a.Namespace == "" && a.Key == "alt" {
if prev == nil {
v := a.Val
prev = &v
}
if set {
continue
}
a.Val = alt
set = true
}
attrs = append(attrs, a)
}
if !set {
attrs = append(attrs, xhtml.Attribute{Key: "alt", Val: alt})
}
tok.Attr = attrs
return prev
}