Update licenses and improve sr-only elements

This commit is contained in:
Bad Manners 2024-08-31 19:02:45 -03:00
parent 287f2cae2f
commit 90fc60e871
Signed by: badmanners
GPG key ID: 8C88292CCB075609
11 changed files with 126 additions and 82 deletions

View file

@ -14,7 +14,10 @@ export default defineConfig({
markdownIntegration(), markdownIntegration(),
htaccessIntegration({ htaccessIntegration({
generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true", generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true",
redirects: [ { match: "/story/", url: "/stories/" }, { match: "/game/", url: "/games/" } ], redirects: [
{ match: "/story/", url: "/stories/" },
{ match: "/game/", url: "/games/" },
],
}), }),
pagefindIntegration(), pagefindIntegration(),
], ],

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"version": "1.7.11", "version": "1.7.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"version": "1.7.11", "version": "1.7.12",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.2", "@astrojs/check": "^0.9.2",

View file

@ -1,7 +1,7 @@
{ {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"type": "module", "type": "module",
"version": "1.7.11", "version": "1.7.12",
"scripts": { "scripts": {
"postinstall": "astro sync", "postinstall": "astro sync",
"dev": "astro dev", "dev": "astro dev",

View file

@ -5,7 +5,7 @@ title = "gallery.badmanners.xyz"
description = "Bad Manners's self-hosted gallery." description = "Bad Manners's self-hosted gallery."
type = "website" type = "website"
date = "2024" date = "2024"
author = "Bad Manners <me@badmanners.xyz>" author = { name = "Bad Manners", url = "https://badmanners.xyz", email = "me@badmanners.xyz>" }
source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz" source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz"
license = { name = "MIT", url = "https://opensource.org/license/mit" } license = { name = "MIT", url = "https://opensource.org/license/mit" }
notes = """ notes = """
@ -13,21 +13,26 @@ All rights reserved.
The MIT License applies only to the source code; see additional copyrights for details.""" The MIT License applies only to the source code; see additional copyrights for details."""
[[copyright.additional]] [[copyright.additional]]
type = "logo"
notes = "The briefcase logo is copyrighted and trademarked by me. All rights reserved." notes = "The briefcase logo is copyrighted and trademarked by me. All rights reserved."
[[copyright.additional]] [[copyright.additional]]
type = "characters"
notes = """ notes = """
My characters, whether directly attributed to me or unattributed, are copyrighted and trademarked by me. My characters, whether directly attributed to me or unattributed, are copyrighted and trademarked by me.
All rights reserved.""" All rights reserved."""
[[copyright.additional]] [[copyright.additional]]
type = "content"
description = "Content hosted on this website, i.e. the stories and game(s)." description = "Content hosted on this website, i.e. the stories and game(s)."
date = "2022-2024" date = "2022-2024"
author = "Bad Manners <me@badmanners.xyz>" author = { name = "Bad Manners", url = "https://badmanners.xyz", email = "me@badmanners.xyz>" }
source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz/src/branch/main/src/content"
license = { name = "CC-BY-NC-ND-4.0", url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" } license = { name = "CC-BY-NC-ND-4.0", url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" }
notes = "All rights reserved." notes = "All rights reserved."
[[copyright.additional]] [[copyright.additional]]
type = "third-party trademarks"
notes = """ notes = """
All third-party copyrights, trademarks, and attributed characters belong to their respective owners, \ All third-party copyrights, trademarks, and attributed characters belong to their respective owners, \
and I'm not affiliated with any of them.""" and I'm not affiliated with any of them."""

View file

@ -49,8 +49,8 @@ const isCurrentRoute = (path: string) =>
class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md" class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
width={192} width={192}
/> />
<span class="p-name mt-2 mb-4 text-2xl font-semibold">Bad Manners</span> <span class="p-name mb-4 mt-2 text-2xl font-semibold">Bad Manners</span>
<ul class="flex flex-col gap-y-2 text-left pr-8 sm:pr-2"> <ul class="flex flex-col gap-y-2 pr-8 text-left sm:pr-2">
<li> <li>
<a class="u-url text-link group" href="https://badmanners.xyz/" data-age-restricted rel="me"> <a class="u-url text-link group" href="https://badmanners.xyz/" data-age-restricted rel="me">
<IconHome width="1.25rem" height="1.25rem" class="inline align-text-top" /> <IconHome width="1.25rem" height="1.25rem" class="inline align-text-top" />

View file

@ -128,7 +128,7 @@ const thumbnail =
aria-labelledby="label-return-to" aria-labelledby="label-return-to"
> >
<IconArrowBack width="1.25rem" height="1.25rem" /> <IconArrowBack width="1.25rem" height="1.25rem" />
<span class="sr-only" id="label-return-to" <span class="sr-only select-none" id="label-return-to"
>{ >{
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
}</span }</span
@ -140,7 +140,7 @@ const thumbnail =
aria-labelledby="label-go-to-description" aria-labelledby="label-go-to-description"
> >
<IconCircleInfo width="1.25rem" height="1.25rem" /> <IconCircleInfo width="1.25rem" height="1.25rem" />
<span class="sr-only" id="label-go-to-description" <span class="sr-only select-none" id="label-go-to-description"
>{t(props.lang, "published_content/go_to_description")}</span >{t(props.lang, "published_content/go_to_description")}</span
> >
</a> </a>
@ -152,7 +152,7 @@ const thumbnail =
> >
<IconSun width="1.25rem" height="1.25rem" class="hidden dark:block" /> <IconSun width="1.25rem" height="1.25rem" class="hidden dark:block" />
<IconMoon width="1.25rem" height="1.25rem" class="block dark:hidden" /> <IconMoon width="1.25rem" height="1.25rem" class="block dark:hidden" />
<span class="sr-only" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span> <span class="sr-only select-none" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
</button> </button>
</div> </div>
</div> </div>

View file

@ -55,7 +55,7 @@ const games = await Promise.all(
</time> </time>
</div> </div>
</a> </a>
<div class="sr-only"> <div class="sr-only select-none">
<p class="p-category" aria-label="Category"> <p class="p-category" aria-label="Category">
Game Game
</p> </p>

View file

@ -133,7 +133,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
</span> </span>
</div> </div>
</a> </a>
<div class="sr-only"> <div class="sr-only select-none">
<p class="p-summary" aria-label="Summary"> <p class="p-summary" aria-label="Summary">
{entry.altText} {entry.altText}
</p> </p>

View file

@ -3,71 +3,88 @@ import { readFile } from "node:fs/promises";
import { parse } from "toml"; import { parse } from "toml";
/** /**
* Verify attributions and copyright according to the [Creative Commons recommended practices](https://wiki.creativecommons.org/wiki/Recommended_practices_for_attribution) * Makes sure the copyright follows the TASL format. T = title (or description), A = author, S = source, L = license.
* @param copyright
*/
function validateTASL(copyright: any) {
const title = copyright.title ?? copyright.description;
if (typeof title !== "string" || !title) {
throw new Error(`Missing "title" and/or "description" for attribution (${JSON.stringify(copyright)})`);
}
// Author must be a valid string or object or list
const authors = [copyright.author].flat();
if (authors.length === 0) {
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
authors.forEach((author) => {
if (!author) {
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
if (typeof author !== "object" && typeof author !== "string") {
throw new Error(
`Invalid "${typeof author}" type for "author"${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
if (typeof author === "object" && !(author.name || author.url)) {
throw new Error(
`Missing both name and URL for "author" ${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
});
// Source must be a valid string or list of strings
const sources = [copyright.source].flat();
if (sources.length === 0) {
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
sources.forEach((source) => {
if (!source) {
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
if (typeof source !== "object" && typeof source !== "string") {
throw new Error(
`Invalid "${typeof source}" type for "source"${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
if (typeof source === "object" && !source.url) {
throw new Error(
`Missing URL for "source" ${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
});
// License must be a valid string or object or list
const licenses = [copyright.license].flat();
if (licenses.length === 0) {
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
licenses.forEach((license) => {
if (!license) {
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(copyright)})`);
}
if (typeof license !== "object" && typeof license !== "string") {
throw new Error(
`Invalid "${typeof license}" type for "license"${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
if (typeof license === "object" && !(license.name || license.url)) {
throw new Error(
`Missing both name and URL for "license" ${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(copyright)})`,
);
}
});
}
/**
* Verifies attributions and copyright according to the [Creative Commons recommended practices](https://wiki.creativecommons.org/wiki/Recommended_practices_for_attribution)
* @param copyright Unparsed TOML copyright information. * @param copyright Unparsed TOML copyright information.
*/ */
function verifyAttributions(licenses: string) { function verifyAttributions(licenses: string) {
const { copyright, attributions } = parse(licenses); const { copyright, attributions } = parse(licenses);
// Make sure each copyright and attribution follows the TASL format. // Make sure each copyright and attribution follows the TASL format,
// - T: title (or description) // and that other fields (type, notes, items) pass their custom validation.
// - A: author
// - S: source
// - L: license
// - other fields that have custom validation: type, notes, items
[copyright, attributions].flat().forEach((value) => { [copyright, attributions].flat().forEach((value) => {
// Title or description must be a valid string // Validate TASL
const title = value.title ?? value.description; validateTASL(value);
if (typeof title !== "string" || !title) { const title = copyright.title ?? copyright.description;
throw new Error(`Missing "title" and/or "description" for attribution (${JSON.stringify(value)})`);
}
// Author must be a valid string or object or list
const authors = [value.author].flat();
if (authors.length === 0) {
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`);
}
authors.forEach((author) => {
if (!author) {
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof author !== "object" && typeof author !== "string") {
throw new Error(`Invalid "${typeof author}" type for "author"${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof author === "object" && !(author.name || author.url)) {
throw new Error(`Missing both name and URL for "author" ${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`);
}
});
// Source must be a valid string or list of strings
const sources = [value.source].flat();
if (sources.length === 0) {
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`);
}
sources.forEach((source) => {
if (!source) {
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof source !== "object" && typeof source !== "string") {
throw new Error(`Invalid "${typeof source}" type for "source"${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof source === "object" && !(source.url)) {
throw new Error(`Missing URL for "source" ${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`);
}
});
// License must be a valid string or object or list
const licenses = [value.license].flat();
if (licenses.length === 0) {
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`);
}
licenses.forEach((license) => {
if (!license) {
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof license !== "object" && typeof license !== "string") {
throw new Error(`Invalid "${typeof license}" type for "license"${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`);
}
if (typeof license === "object" && !(license.name || license.url)) {
throw new Error(`Missing both name and URL for "license" ${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`);
}
});
// Validate extra optional fields // Validate extra optional fields
// 1. Type must be a valid string // 1. Type must be a valid string
if (typeof value.type !== "string") { if (typeof value.type !== "string") {
@ -80,13 +97,17 @@ function verifyAttributions(licenses: string) {
if ("items" in value) { if ("items" in value) {
const items = value.items; const items = value.items;
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
throw new Error(`Invalid non-array "items" ${JSON.stringify(items)} for attribution "${title}" (${JSON.stringify(value)})`); throw new Error(
`Invalid non-array "items" ${JSON.stringify(items)} for attribution "${title}" (${JSON.stringify(value)})`,
);
} }
items.forEach((item) => { items.forEach((item) => {
if (!item) { if (!item) {
throw new Error(`Invalid item ${JSON.stringify} in "items" for attribution "${title}" (${JSON.stringify(value)})`); throw new Error(
`Invalid item ${JSON.stringify} in "items" for attribution "${title}" (${JSON.stringify(value)})`,
);
} }
}) });
} }
// 3. Type must be a valid string // 3. Type must be a valid string
if ("notes" in value) { if ("notes" in value) {
@ -106,18 +127,33 @@ function verifyAttributions(licenses: string) {
if ("additional" in copyright) { if ("additional" in copyright) {
const additionals = copyright.additional; const additionals = copyright.additional;
if (!Array.isArray(additionals)) { if (!Array.isArray(additionals)) {
throw new Error(`Invalid non-array "additional" ${JSON.stringify(additionals)} for copyright (${JSON.stringify(copyright)})`); throw new Error(
`Invalid non-array "additional" ${JSON.stringify(additionals)} for copyright (${JSON.stringify(copyright)})`,
);
} }
additionals.forEach((additional) => { additionals.forEach((additional) => {
if (typeof additional.notes !== "string" || !additional.notes) { if (typeof additional.notes !== "string" || !additional.notes) {
throw new Error(`Invalid "notes" for additional copyright (${JSON.stringify(additional)})`); throw new Error(`Invalid "notes" for additional copyright (${JSON.stringify(additional)})`);
} }
}) if (typeof additional.type !== "string" || !additional.type) {
throw new Error(`Invalid "type" for additional copyright (${JSON.stringify(additional)})`);
}
// Check TASL + date if title or description is present
if (additional.title || additional.description) {
validateTASL(additional);
if (typeof additional.date !== "string") {
throw new Error(`Invalid "date" for additional (${JSON.stringify(additional)})`);
}
if (!additional.date) {
throw new Error(`Missing "date" for additional (${JSON.stringify(additional)})`);
}
}
});
} }
} }
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {
const licenses = await readFile("./src/data/licenses.toml", { encoding: "utf-8" }); const licenses = await readFile("./src/data/licenses.toml", { encoding: "utf-8" });
verifyAttributions(licenses); verifyAttributions(licenses);
return new Response(licenses, { headers: { "Content-Type": "application/toml; charset=utf-8" } }) return new Response(licenses, { headers: { "Content-Type": "application/toml; charset=utf-8" } });
}; };

View file

@ -113,7 +113,7 @@ const totalPages = Math.ceil(page.total / page.size);
</time> </time>
</div> </div>
</a> </a>
<div class="sr-only"> <div class="sr-only select-none">
<p class="p-category" aria-label="Category"> <p class="p-category" aria-label="Category">
Story Story
</p> </p>

View file

@ -177,7 +177,7 @@ const totalWorksWithTag = t(
</time> </time>
</div> </div>
</a> </a>
<div class="sr-only"> <div class="sr-only select-none">
<p class="p-category" aria-label="Category"> <p class="p-category" aria-label="Category">
Story Story
</p> </p>
@ -235,7 +235,7 @@ const totalWorksWithTag = t(
</time> </time>
</div> </div>
</a> </a>
<div class="sr-only"> <div class="sr-only select-none">
<p class="p-category" aria-label="Category"> <p class="p-category" aria-label="Category">
Game Game
</p> </p>