fix: image alt texts on markdown posts are written into the markdown

POST /_journal/api/v1/images/alt writes the alt of a markdown post into
its source: the inline image ![alt](src) outside code
(classes.SetMarkdownImageAlt) or a raw img outside code
(classes.SetImageAltOutsideCode), so the next content save keeps it. An
image the markdown does not hold that way is refused with 422
changes.N.src and nothing is written.
This commit is contained in:
Jakub Zych
2026-11-04 23:20:02 +01:00
parent 5f683cba31
commit 7662d54ab3
6 changed files with 449 additions and 8 deletions

View File

@@ -1,6 +1,9 @@
package classes
import "testing"
import (
"strings"
"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
@@ -54,3 +57,53 @@ func TestSetImageAltEdges(t *testing.T) {
t.Errorf("match with an unfinished tail = %q %d", out, n)
}
}
// TestSetMarkdownImageAlt pins P33-65 (L1 WR-05): inline images with or
// without a title get the alt; a destination written unescaped matches the
// src goldmark renders for it; an alt with ] and a line break is escaped
// and folded; the same image in a fence or a code span is left alone; a
// reference-style image is not matched.
func TestSetMarkdownImageAlt(t *testing.T) {
out, n, old := SetMarkdownImageAlt("A ![](/a.jpg) and ![old](/a.jpg \"t\") and ![x](/b.jpg).", "/a.jpg", "Lion's mane")
if n != 2 || old == nil || *old != "" || out != "A ![Lion's mane](/a.jpg) and ![Lion's mane](/a.jpg \"t\") and ![x](/b.jpg)." {
t.Fatalf("plain = %q %d %v", out, n, old)
}
html, err := FormatHTML("![x](</a b.jpg>)")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(html, `src="/a%20b.jpg"`) {
t.Fatalf("goldmark src = %s", html)
}
if out, n, _ := SetMarkdownImageAlt("![x](</a b.jpg>)", "/a%20b.jpg", "y"); n != 1 || out != "![y](</a b.jpg>)" {
t.Fatalf("escaped src = %q %d", out, n)
}
if out, n, _ := SetMarkdownImageAlt("![x](/ż.jpg)", "/%C5%BC.jpg", "y"); n != 1 || out != "![y](/ż.jpg)" {
t.Fatalf("non-ASCII src = %q %d", out, n)
}
if out, _, _ := SetMarkdownImageAlt("![x](/a.jpg)", "/a.jpg", "a ] b\nc [d] \\e"); out != `![a \] b c \[d\] \\e](/a.jpg)` {
t.Fatalf("escaped alt = %q", out)
}
code := "```\n![x](/a.jpg)\n```\n\nInline `![x](/a.jpg)` and\n\n ![x](/a.jpg)\n"
if out, n, _ := SetMarkdownImageAlt(code, "/a.jpg", "y"); n != 0 || out != code {
t.Fatalf("code = %q %d", out, n)
}
ref := "![x][a]\n\n[a]: /a.jpg\n"
if out, n, _ := SetMarkdownImageAlt(ref, "/a.jpg", "y"); n != 0 || out != ref {
t.Fatalf("reference = %q %d", out, n)
}
// The rendered alt follows the markdown.
out, _, _ = SetMarkdownImageAlt("Text\n\n![](/a.jpg)\n", "/a.jpg", "Grzyb [leśny]")
if html, _ := FormatHTML(out); !strings.Contains(html, `alt="Grzyb [leśny]"`) {
t.Fatalf("rendered = %s", html)
}
// SetImageAltOutsideCode: a raw img gets the alt, one in a code sample
// keeps its bytes.
raw := "<img src=\"/a.jpg\">\n\n```html\n<img src=\"/a.jpg\">\n```\n\nand `<img src=\"/a.jpg\">`\n"
got, n, _ := SetImageAltOutsideCode(raw, "/a.jpg", "z")
want := "<img src=\"/a.jpg\" alt=\"z\">\n\n```html\n<img src=\"/a.jpg\">\n```\n\nand `<img src=\"/a.jpg\">`\n"
if n != 1 || got != want {
t.Fatalf("outside code = %q %d", got, n)
}
}