Commit 68df7ac580
Verified · cmc ci/build: success ci/lint: success ci/test: success
Layout: unified · split
app/escape_test.go +27 −2
| @@ -109,8 +109,8 @@ func TestParseCommentsEscapesUsernames(t *testing.T) { | |||
| 109 | if strings.Contains(out, "<b id=injected>") { | 109 | if strings.Contains(out, "<b id=injected>") { |
| 110 | t.Fatalf("username rendered as markup:\n%s", out) | 110 | t.Fatalf("username rendered as markup:\n%s", out) |
| 111 | } | 111 | } |
| 112 | if n := strings.Count(out, "<b id=injected>"); n != 4 { | 112 | if n := strings.Count(out, "<b id=injected>"); n != 5 { |
| 113 | t.Errorf("escaped username appears %d times, want 4 (avatar, link, author, reply target):\n%s", n, out) | 113 | t.Errorf("escaped username appears %d times, want 5 (avatar src and alt, link, author, reply target):\n%s", n, out) |
| 114 | } | 114 | } |
| 115 | } | 115 | } |
| 116 | 116 | ||
| @@ -162,3 +162,28 @@ func TestExecuteTemplateUsesTheRequestLanguage(t *testing.T) { | |||
| 162 | t.Errorf("Spanish request rendered without the Spanish catalogue:\n%s", rec.Body.String()) | 162 | t.Errorf("Spanish request rendered without the Spanish catalogue:\n%s", rec.Body.String()) |
| 163 | } | 163 | } |
| 164 | } | 164 | } |
| 165 | |||
| 166 | // TestListingImagesCarryAltText covers the accessibility fix: every image the | ||
| 167 | // Go builders emit names what it shows. | ||
| 168 | func TestListingImagesCarryAltText(t *testing.T) { | ||
| 169 | nsfw := CFG.Nsfw | ||
| 170 | CFG.Nsfw = true | ||
| 171 | defer func() { CFG.Nsfw = nsfw }() | ||
| 172 | |||
| 173 | d := *fullviewDeviation() | ||
| 174 | d.Title = "T" | ||
| 175 | d.Author.Username = "alice" | ||
| 176 | if out := (skunkyart{Host: "http://localhost"}).DeviationList([]devianter.Deviation{d}, false); !strings.Contains(out, `alt="alice - T"`) { | ||
| 177 | t.Errorf("listing image has no alt text:\n%s", out) | ||
| 178 | } | ||
| 179 | if out := BuildUserPlate("http://localhost", "bob"); !strings.Contains(out, `alt="bob"`) { | ||
| 180 | t.Errorf("user plate image has no alt text:\n%s", out) | ||
| 181 | } | ||
| 182 | var c devianter.Comments | ||
| 183 | var th devianter.Thread | ||
| 184 | th.User.Username = "carol" | ||
| 185 | c.Thread = []devianter.Thread{th} | ||
| 186 | if out := (skunkyart{Host: "http://localhost"}).ParseComments(c, devianter.Error{}); !strings.Contains(out, `alt="carol"`) { | ||
| 187 | t.Errorf("comment avatar has no alt text:\n%s", out) | ||
| 188 | } | ||
| 189 | } | ||
app/parsers.go +12 −2
| @@ -36,7 +36,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) | |||
| 36 | cmmts.WriteString(strconv.Itoa(x.ID)) | 36 | cmmts.WriteString(strconv.Itoa(x.ID)) |
| 37 | cmmts.WriteString(`"><img src="`) | 37 | cmmts.WriteString(`"><img src="`) |
| 38 | cmmts.WriteString(esc(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a"))) | 38 | cmmts.WriteString(esc(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a"))) |
| 39 | cmmts.WriteString(`" width="30px" height="30px"><a href="`) | 39 | cmmts.WriteString(`" width="30px" height="30px" alt="`) |
| 40 | cmmts.WriteString(esc(x.User.Username)) | ||
| 41 | cmmts.WriteString(`"><a href="`) | ||
| 40 | cmmts.WriteString(esc(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a"))) | 42 | cmmts.WriteString(esc(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a"))) |
| 41 | cmmts.WriteString(`"><b`) | 43 | cmmts.WriteString(`"><b`) |
| 42 | cmmts.WriteString(` class="`) | 44 | cmmts.WriteString(` class="`) |
| @@ -160,7 +162,11 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con | |||
| 160 | listContent.WriteString(fullview) | 162 | listContent.WriteString(fullview) |
| 161 | listContent.WriteString(`"><img loading="lazy" src="`) | 163 | listContent.WriteString(`"><img loading="lazy" src="`) |
| 162 | listContent.WriteString(preview) | 164 | listContent.WriteString(preview) |
| 163 | listContent.WriteString(`" width="15%"></a>`) | 165 | listContent.WriteString(`" width="15%" alt="`) |
| 166 | listContent.WriteString(author) | ||
| 167 | listContent.WriteString(" - ") | ||
| 168 | listContent.WriteString(title) | ||
| 169 | listContent.WriteString(`"></a>`) | ||
| 164 | } else { | 170 | } else { |
| 165 | listContent.WriteString("<h1>[ " + esc(T(s.Lang, "list.text")) + " ]</h1>") | 171 | listContent.WriteString("<h1>[ " + esc(T(s.Lang, "list.text")) + " ]</h1>") |
| 166 | } | 172 | } |
| @@ -361,6 +367,10 @@ func ParseDescription(host string, dscr devianter.Text) string { | |||
| 361 | parsedDescription.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(host, d.Url))) | 367 | parsedDescription.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(host, d.Url))) |
| 362 | parsedDescription.WriteString(`"><img width="50%" src="`) | 368 | parsedDescription.WriteString(`"><img width="50%" src="`) |
| 363 | parsedDescription.WriteString(esc(ParseMedia(host, d.Media))) | 369 | parsedDescription.WriteString(esc(ParseMedia(host, d.Media))) |
| 370 | parsedDescription.WriteString(`" alt="`) | ||
| 371 | parsedDescription.WriteString(esc(d.Author.Username)) | ||
| 372 | parsedDescription.WriteString(" - ") | ||
| 373 | parsedDescription.WriteString(esc(d.Title)) | ||
| 364 | parsedDescription.WriteString(`" title="`) | 374 | parsedDescription.WriteString(`" title="`) |
| 365 | parsedDescription.WriteString(esc(d.Author.Username)) | 375 | parsedDescription.WriteString(esc(d.Author.Username)) |
| 366 | parsedDescription.WriteString(" - ") | 376 | parsedDescription.WriteString(" - ") |
app/util.go +2
| @@ -365,6 +365,8 @@ func BuildUserPlate(host, name string) string { | |||
| 365 | var htm strings.Builder | 365 | var htm strings.Builder |
| 366 | htm.WriteString(`<div class="user-plate"><img src="`) | 366 | htm.WriteString(`<div class="user-plate"><img src="`) |
| 367 | htm.WriteString(esc(URLBuilder(host, "media", "emojitar", name, "?type=a"))) | 367 | htm.WriteString(esc(URLBuilder(host, "media", "emojitar", name, "?type=a"))) |
| 368 | htm.WriteString(`" alt="`) | ||
| 369 | htm.WriteString(esc(name)) | ||
| 368 | htm.WriteString(`"><a href="`) | 370 | htm.WriteString(`"><a href="`) |
| 369 | htm.WriteString(esc(URLBuilder(host, "group_user", "?type=about&q=", name))) | 371 | htm.WriteString(esc(URLBuilder(host, "group_user", "?type=about&q=", name))) |
| 370 | htm.WriteString(`">`) | 372 | htm.WriteString(`">`) |
app/wrapper.go +2
| @@ -184,6 +184,8 @@ func (s skunkyart) GRUser() { | |||
| 184 | folders.WriteString(esc(ParseMedia(s.Host, x.Thumb.Media))) | 184 | folders.WriteString(esc(ParseMedia(s.Host, x.Thumb.Media))) |
| 185 | folders.WriteString(`" title="`) | 185 | folders.WriteString(`" title="`) |
| 186 | folders.WriteString(esc(x.Thumb.Title)) | 186 | folders.WriteString(esc(x.Thumb.Title)) |
| 187 | folders.WriteString(`" alt="`) | ||
| 188 | folders.WriteString(esc(x.Thumb.Title)) | ||
| 187 | folders.WriteString(`"></a>`) | 189 | folders.WriteString(`"></a>`) |
| 188 | } else { | 190 | } else { |
| 189 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) | 191 | folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`) |
static/css/skunky.css +82 −14
| @@ -187,25 +187,15 @@ input:focus { | |||
| 187 | 187 | ||
| 188 | /* SCREEN OPTIMISATIONS */ | 188 | /* SCREEN OPTIMISATIONS */ |
| 189 | @media (orientation: portrait) { | 189 | @media (orientation: portrait) { |
| 190 | * { | ||
| 191 | font-size: 120% | ||
| 192 | } | ||
| 193 | |||
| 194 | ul { | ||
| 195 | font-size: 80% | ||
| 196 | } | ||
| 197 | |||
| 198 | header { | 190 | header { |
| 199 | margin-left: 3%; | 191 | margin-left: 3%; |
| 200 | text-align: center; | 192 | text-align: center; |
| 201 | display: inline-block; | 193 | display: inline-block; |
| 202 | clear: both; | 194 | clear: both; |
| 203 | font-size: 200%; | ||
| 204 | } | 195 | } |
| 205 | 196 | ||
| 206 | form { | 197 | form { |
| 207 | font-size: 60%; | 198 | border: solid var(--edge) 1px; |
| 208 | border: solid var(--edge) 5px; | ||
| 209 | } | 199 | } |
| 210 | 200 | ||
| 211 | .content { | 201 | .content { |
| @@ -215,7 +205,7 @@ input:focus { | |||
| 215 | } | 205 | } |
| 216 | .block { | 206 | .block { |
| 217 | margin-top: 10%; | 207 | margin-top: 10%; |
| 218 | max-width: 200%; | 208 | max-width: 100%; |
| 219 | } | 209 | } |
| 220 | .folder-item { | 210 | .folder-item { |
| 221 | width: 25% | 211 | width: 25% |
| @@ -231,7 +221,6 @@ input:focus { | |||
| 231 | width: 100% | 221 | width: 100% |
| 232 | } | 222 | } |
| 233 | .msg { | 223 | .msg { |
| 234 | font-size: 60%; | ||
| 235 | max-width: 80% | 224 | max-width: 80% |
| 236 | } | 225 | } |
| 237 | .block img, .plates .user-plate img { | 226 | .block img, .plates .user-plate img { |
| @@ -260,4 +249,83 @@ input:focus { | |||
| 260 | .block { | 249 | .block { |
| 261 | max-width: 35%; | 250 | max-width: 35%; |
| 262 | } | 251 | } |
| 263 | }\ No newline at end of file | 252 | } |
| 253 | /* POST */ | ||
| 254 | figure h1 { | ||
| 255 | font-size: 1.2em; | ||
| 256 | margin: 0.3em 0; | ||
| 257 | } | ||
| 258 | |||
| 259 | /* INDEX */ | ||
| 260 | .index { | ||
| 261 | display: flex; | ||
| 262 | max-width: fit-content; | ||
| 263 | position: absolute; | ||
| 264 | top: 50%; | ||
| 265 | right: 50%; | ||
| 266 | transform: translate(50%, -50%); | ||
| 267 | } | ||
| 268 | .index div { | ||
| 269 | transform: translate(0, 50%); | ||
| 270 | margin-left: 4%; | ||
| 271 | flex-basis: 100%; | ||
| 272 | height: 30%; | ||
| 273 | display: block; | ||
| 274 | max-width: fit-content; | ||
| 275 | } | ||
| 276 | .index div h1, .index form { | ||
| 277 | margin: 0; | ||
| 278 | } | ||
| 279 | .index div form { | ||
| 280 | font-size: 100%; | ||
| 281 | max-width: 100%; | ||
| 282 | } | ||
| 283 | .index div form input { | ||
| 284 | width: 100%; | ||
| 285 | } | ||
| 286 | .index img { | ||
| 287 | width: 30%; | ||
| 288 | height: 30%; | ||
| 289 | } | ||
| 290 | .index .source { | ||
| 291 | margin-top: 5%; | ||
| 292 | font-size: 200%; | ||
| 293 | text-align: center; | ||
| 294 | } | ||
| 295 | @media (orientation: portrait) { | ||
| 296 | .index { | ||
| 297 | display: block; | ||
| 298 | position: static; | ||
| 299 | transform: none; | ||
| 300 | width: 100%; | ||
| 301 | max-width: none; | ||
| 302 | padding: 1em; | ||
| 303 | box-sizing: border-box; | ||
| 304 | } | ||
| 305 | .index img { | ||
| 306 | width: 60%; | ||
| 307 | height: auto; | ||
| 308 | display: block; | ||
| 309 | margin: 0 auto; | ||
| 310 | } | ||
| 311 | .index form { | ||
| 312 | width: 100%; | ||
| 313 | } | ||
| 314 | .index div { | ||
| 315 | transform: none; | ||
| 316 | margin: 0; | ||
| 317 | width: 100%; | ||
| 318 | max-width: none; | ||
| 319 | } | ||
| 320 | .index div h1 { | ||
| 321 | text-align: center; | ||
| 322 | } | ||
| 323 | } | ||
| 324 | @media (max-width: 1155px) and (orientation: landscape) { | ||
| 325 | .index img { | ||
| 326 | width: 50%; | ||
| 327 | } | ||
| 328 | .index div { | ||
| 329 | transform: none; | ||
| 330 | } | ||
| 331 | } | ||
static/html/deviantion.htm +2 −2
| @@ -5,12 +5,12 @@ | |||
| 5 | {{template "header" . }} | 5 | {{template "header" . }} |
| 6 | <figure> | 6 | <figure> |
| 7 | <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px" alt="{{.Templates.Deviation.Post.Deviation.Author.Username}} avatar"> | 7 | <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px" alt="{{.Templates.Deviation.Post.Deviation.Author.Username}} avatar"> |
| 8 | <span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}} | 8 | <h1><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}} |
| 9 | <span class="dd" title="{{T "deviation.daily"}}"><b>{{.Templates.Deviation.Post.Deviation.Title}}</b></span> | 9 | <span class="dd" title="{{T "deviation.daily"}}"><b>{{.Templates.Deviation.Post.Deviation.Title}}</b></span> |
| 10 | {{else}}{{.Templates.Deviation.Post.Deviation.Title}}{{end}} | 10 | {{else}}{{.Templates.Deviation.Post.Deviation.Title}}{{end}} |
| 11 | {{if (ne .Templates.Deviation.Post.Deviation.License "none")}}<mark title="{{T "deviation.license"}}">{{.Templates.Deviation.Post.Deviation.License}}</mark>{{end}} {{if (.Templates.Deviation.Post.Deviation.AI)}}[🤖]{{end}} | 11 | {{if (ne .Templates.Deviation.Post.Deviation.License "none")}}<mark title="{{T "deviation.license"}}">{{.Templates.Deviation.Post.Deviation.License}}</mark>{{end}} {{if (.Templates.Deviation.Post.Deviation.AI)}}[🤖]{{end}} |
| 12 | {{if (.Templates.Deviation.Post.Deviation.NSFW)}}[<span class="nsfw">{{T "deviation.nsfw"}}</span>]{{end}} | 12 | {{if (.Templates.Deviation.Post.Deviation.NSFW)}}[<span class="nsfw">{{T "deviation.nsfw"}}</span>]{{end}} |
| 13 | </span> | 13 | </h1> |
| 14 | <br> | 14 | <br> |
| 15 | {{if (ne .Templates.Deviation.Post.IMG "")}} | 15 | {{if (ne .Templates.Deviation.Post.IMG "")}} |
| 16 | <a href="{{.Templates.Deviation.Post.IMG}}" title="{{T "deviation.open"}}"><img src="{{.Templates.Deviation.Post.IMG}}" width="50%" alt="{{.Templates.Deviation.Post.Deviation.Title}}"></a> | 16 | <a href="{{.Templates.Deviation.Post.IMG}}" title="{{T "deviation.open"}}"><img src="{{.Templates.Deviation.Post.IMG}}" width="50%" alt="{{.Templates.Deviation.Post.Deviation.Title}}"></a> |
static/html/head.htm +1 −1
| @@ -21,6 +21,6 @@ | |||
| 21 | <meta name="referrer" content="no-referrer" /> | 21 | <meta name="referrer" content="no-referrer" /> |
| 22 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"> | 22 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"> |
| 23 | <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico"> | 23 | <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico"> |
| 24 | <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4"/> | 24 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
| 25 | </head> | 25 | </head> |
| 26 | {{end}}\ No newline at end of file | 26 | {{end}}\ No newline at end of file |
static/html/index.htm +3 −65
| @@ -4,72 +4,10 @@ | |||
| 4 | <title>SkunkyArt</title> | 4 | <title>SkunkyArt</title> |
| 5 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"/> | 5 | <link rel="stylesheet" href="{{.BasePath}}stylesheet"/> |
| 6 | <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico"> | 6 | <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico"> |
| 7 | <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4"/> | 7 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
| 8 | 8 | ||
| 9 | <style> | ||
| 10 | main { | ||
| 11 | display: flex; | ||
| 12 | max-width: fit-content; | ||
| 13 | position: absolute; | ||
| 14 | top: 50%; | ||
| 15 | right: 50%; | ||
| 16 | transform: translate(50%, -50%); | ||
| 17 | } | ||
| 18 | div { | ||
| 19 | transform: translate(0, 50%); | ||
| 20 | margin-left: 4%; | ||
| 21 | flex-basis: 100%; | ||
| 22 | height: 30%; | ||
| 23 | display: block; | ||
| 24 | max-width: fit-content; | ||
| 25 | } | ||
| 26 | div h1, form { | ||
| 27 | margin: 0; | ||
| 28 | } | ||
| 29 | div form { | ||
| 30 | font-size: 100%; | ||
| 31 | max-width: 100% | ||
| 32 | } | ||
| 33 | div form input { | ||
| 34 | width: 100% | ||
| 35 | } | ||
| 36 | img { | ||
| 37 | width: 30%; | ||
| 38 | height: 30%; | ||
| 39 | } | ||
| 40 | |||
| 41 | @media (orientation: portrait) { | ||
| 42 | main { | ||
| 43 | display: block; | ||
| 44 | width: 200%; | ||
| 45 | } | ||
| 46 | img { | ||
| 47 | width: 100%; | ||
| 48 | height: 100%; | ||
| 49 | } | ||
| 50 | form { | ||
| 51 | width: 200%; | ||
| 52 | } | ||
| 53 | div { | ||
| 54 | margin: -25%; | ||
| 55 | margin-top: auto; | ||
| 56 | width: 200%; | ||
| 57 | } | ||
| 58 | div h1 { | ||
| 59 | text-align: center; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | @media (max-width: 1155px) and (orientation: landscape) { | ||
| 63 | img { | ||
| 64 | width: 50%; | ||
| 65 | } | ||
| 66 | div { | ||
| 67 | transform: none; | ||
| 68 | } | ||
| 69 | } | ||
| 70 | </style> | ||
| 71 | </head> | 9 | </head> |
| 72 | <main> | 10 | <main class="index"> |
| 73 | <img src="{{.BasePath}}favicon.ico" title="{{T "index.logo"}}" alt="SkunkyArt logo" draggable="false"> | 11 | <img src="{{.BasePath}}favicon.ico" title="{{T "index.logo"}}" alt="SkunkyArt logo" draggable="false"> |
| 74 | <div> | 12 | <div> |
| 75 | <h1><a href="{{.BasePath}}dd">{{T "nav.daily"}}</a> | <a href="{{.BasePath}}about">{{T "nav.about"}}</a></h1> | 13 | <h1><a href="{{.BasePath}}dd">{{T "nav.daily"}}</a> | <a href="{{.BasePath}}about">{{T "nav.about"}}</a></h1> |
| @@ -82,7 +20,7 @@ | |||
| 82 | </select> | 20 | </select> |
| 83 | <button type="submit">{{T "search.submit"}}</button> | 21 | <button type="submit">{{T "search.submit"}}</button> |
| 84 | </form> | 22 | </form> |
| 85 | <h1 style="margin-top: 5%; font-size: 200%; text-align: center;"> | 23 | <h1 class="source"> |
| 86 | <a href="https://github.com/krazywarez/skunky-art" target="_blank" title="{{T "index.source"}}">SkunkyArt</a> | 24 | <a href="https://github.com/krazywarez/skunky-art" target="_blank" title="{{T "index.source"}}">SkunkyArt</a> |
| 87 | </h1> | 25 | </h1> |
| 88 | </div> | 26 | </div> |