feat: MCP tools for settings, headings and image alt
- POST /_journal/api/v1/images/alt {dry_run, changes} sets alt on the
matching img of default or translated content through the tokenizer,
all or nothing (422), with a RecordSaved per changed post
- MCP journal_get_settings, journal_update_settings, journal_fix_headings
(manage_settings) and journal_set_image_alt (access_posts, destructive)
- journal:fix-headings help names its output and the refusal
This commit is contained in:
92
classes/image_alt.go
Normal file
92
classes/image_alt.go
Normal file
@@ -0,0 +1,92 @@
|
||||
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. 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
|
||||
for {
|
||||
tt := z.Next()
|
||||
if tt == xhtml.ErrorToken {
|
||||
break
|
||||
}
|
||||
raw := z.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
|
||||
}
|
||||
22
classes/image_alt_test.go
Normal file
22
classes/image_alt_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package classes
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestSetImageAlt pins the alt rewrite: only the matching img tag is
|
||||
// rewritten from its token, the alt is escaped, a missing alt is added and
|
||||
// every other byte is kept (T-33-04).
|
||||
func TestSetImageAlt(t *testing.T) {
|
||||
const doc = `<p class="a">x & y</p><img src="https://cdn.example.test/a.jpg?x=1&y=2"><img src="b.jpg" alt="old"/>`
|
||||
out, n, old := SetImageAlt(doc, "https://cdn.example.test/a.jpg?x=1&y=2", `Lion's "mane" <b>`)
|
||||
want := `<p class="a">x & y</p><img src="https://cdn.example.test/a.jpg?x=1&y=2" alt="Lion's "mane" <b>"><img src="b.jpg" alt="old"/>`
|
||||
if out != want || n != 1 || old != nil {
|
||||
t.Fatalf("SetImageAlt = %q, %d, %v; want %q, 1, nil", out, n, old, want)
|
||||
}
|
||||
out, n, old = SetImageAlt(doc, "b.jpg", "")
|
||||
if out != `<p class="a">x & y</p><img src="https://cdn.example.test/a.jpg?x=1&y=2"><img src="b.jpg" alt=""/>` || n != 1 || old == nil || *old != "old" {
|
||||
t.Fatalf("decorative = %q, %d, %v", out, n, old)
|
||||
}
|
||||
if out, n, _ := SetImageAlt(doc, "missing.jpg", "x"); out != doc || n != 0 {
|
||||
t.Fatalf("missing = %q, %d", out, n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user