fix: image alt on HTML-only translations and on markdown that starts with a tag

Blank content means content_html is the body (no 422 for
content_html-only translations, a regression from the markdown alt fix);
any other content is handled as markdown, so a body opening with a tag
gets its inline image label and keeps its code samples.

quick-261104-xae, review WR-02.
This commit is contained in:
Jakub Zych
2026-11-05 00:19:28 +01:00
parent a41d537e52
commit cf13c606f8
3 changed files with 104 additions and 16 deletions

View File

@@ -97,12 +97,13 @@ var errForbiddenPost = errors.New("forbidden post")
// [{post_id, locale, src, alt}]}: it sets the alt attribute of the img whose
// src equals src in the post's content_html, in the default locale when
// locale is empty and in that translation otherwise; alt "" marks a
// decorative image. The post's content gets it too: an HTML body when it
// holds the same img; a markdown body in its inline image ![alt](src) or a
// raw img outside code (classes.SetMarkdownImageAlt,
// classes.SetImageAltOutsideCode), so the next content save keeps it. An
// image the markdown does not hold that way (a reference-style image, for
// example) refuses the change with 422 changes.N.src. It needs
// decorative image. The post's content gets it too, read as markdown (an
// HTML body included): its inline image ![alt](src) or a raw img outside
// code (classes.SetMarkdownImageAlt, classes.SetImageAltOutsideCode), so
// the next content save keeps it. An image the markdown does not hold that
// way (a reference-style image, for example) refuses the change with 422
// changes.N.src. A locale whose content is blank (a translation that
// carries only content_html) gets the alt in content_html alone. It needs
// golem15.journal.access_posts and may edit only the posts the caller may
// edit (403). The batch is all or nothing: a missing post or an src that is
// not in the post refuses it with 422 (errors keyed changes.N.field). It
@@ -169,15 +170,22 @@ func SetImageAlt(app *backpack.App) http.HandlerFunc {
verrs = addSettingsError(verrs, fmt.Sprintf("changes.%d.src", i), "The image is not in the post.")
continue
}
if classes.LooksLikeHTML(text.content) {
// An HTML body (the rich editor's) holds the same img.
if content, m, _ := classes.SetImageAlt(text.content, c.src, c.alt); m > 0 {
text.content = content
}
} else {
// A markdown body: the next save derives content_html
// from it again, so the alt must land in the markdown
// (L1 WR-05): an inline image, or a raw img outside code.
switch {
case strings.TrimSpace(text.content) == "":
// content_html is the body (a translation that holds
// only content_html, or a row without content): nothing
// else to keep in sync (33-REVIEW-GAPFIX WR-02).
default:
// content is read as markdown, as FormatHTML renders it
// on every API path, and the next save derives
// content_html from it again, so the alt must land in it
// (L1 WR-05): an inline image, or a raw img outside
// code. An HTML body (the rich editor's) is HTML blocks
// to goldmark, so its img is a raw img outside code and
// gets the alt here too. The first character no longer
// picks the branch, so markdown that opens with a tag
// keeps its code samples and is never answered changed
// and reverted later.
content, m, _ := classes.SetMarkdownImageAlt(text.content, c.src, c.alt)
content, r, _ := classes.SetImageAltOutsideCode(content, c.src, c.alt)
if m+r == 0 {