From 90fc60e871aa5d63b2027829b921dc283ac61478 Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Sat, 31 Aug 2024 19:02:45 -0300 Subject: [PATCH] Update licenses and improve sr-only elements --- astro.config.mjs | 5 +- package-lock.json | 4 +- package.json | 2 +- src/data/licenses.toml | 9 +- src/layouts/GalleryLayout.astro | 4 +- src/layouts/PublishedContentLayout.astro | 6 +- src/pages/games.astro | 2 +- src/pages/index.astro | 2 +- src/pages/licenses.toml.ts | 168 ++++++++++++++--------- src/pages/stories/[page].astro | 2 +- src/pages/tags/[slug].astro | 4 +- 11 files changed, 126 insertions(+), 82 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 007d541..afc1300 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,7 +14,10 @@ export default defineConfig({ markdownIntegration(), htaccessIntegration({ 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(), ], diff --git a/package-lock.json b/package-lock.json index 9ab53c4..a0b46b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery.badmanners.xyz", - "version": "1.7.11", + "version": "1.7.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery.badmanners.xyz", - "version": "1.7.11", + "version": "1.7.12", "hasInstallScript": true, "dependencies": { "@astrojs/check": "^0.9.2", diff --git a/package.json b/package.json index 24764f3..1b441c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery.badmanners.xyz", "type": "module", - "version": "1.7.11", + "version": "1.7.12", "scripts": { "postinstall": "astro sync", "dev": "astro dev", diff --git a/src/data/licenses.toml b/src/data/licenses.toml index 05531e0..fb5dc6a 100644 --- a/src/data/licenses.toml +++ b/src/data/licenses.toml @@ -5,7 +5,7 @@ title = "gallery.badmanners.xyz" description = "Bad Manners's self-hosted gallery." type = "website" 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" license = { name = "MIT", url = "https://opensource.org/license/mit" } notes = """ @@ -13,21 +13,26 @@ All rights reserved. The MIT License applies only to the source code; see additional copyrights for details.""" [[copyright.additional]] +type = "logo" notes = "The briefcase logo is copyrighted and trademarked by me. All rights reserved." [[copyright.additional]] +type = "characters" notes = """ My characters, whether directly attributed to me or unattributed, are copyrighted and trademarked by me. All rights reserved.""" [[copyright.additional]] +type = "content" description = "Content hosted on this website, i.e. the stories and game(s)." 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/" } notes = "All rights reserved." [[copyright.additional]] +type = "third-party trademarks" notes = """ All third-party copyrights, trademarks, and attributed characters belong to their respective owners, \ and I'm not affiliated with any of them.""" diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index a73b22f..c82927f 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -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" width={192} /> - <span class="p-name mt-2 mb-4 text-2xl font-semibold">Bad Manners</span> - <ul class="flex flex-col gap-y-2 text-left pr-8 sm:pr-2"> + <span class="p-name mb-4 mt-2 text-2xl font-semibold">Bad Manners</span> + <ul class="flex flex-col gap-y-2 pr-8 text-left sm:pr-2"> <li> <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" /> diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro index c58cbba..36a1a85 100644 --- a/src/layouts/PublishedContentLayout.astro +++ b/src/layouts/PublishedContentLayout.astro @@ -128,7 +128,7 @@ const thumbnail = aria-labelledby="label-return-to" > <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 }</span @@ -140,7 +140,7 @@ const thumbnail = aria-labelledby="label-go-to-description" > <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 > </a> @@ -152,7 +152,7 @@ const thumbnail = > <IconSun width="1.25rem" height="1.25rem" class="hidden dark:block" /> <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> </div> </div> diff --git a/src/pages/games.astro b/src/pages/games.astro index 8554bea..bb766a0 100644 --- a/src/pages/games.astro +++ b/src/pages/games.astro @@ -55,7 +55,7 @@ const games = await Promise.all( </time> </div> </a> - <div class="sr-only"> + <div class="sr-only select-none"> <p class="p-category" aria-label="Category"> Game </p> diff --git a/src/pages/index.astro b/src/pages/index.astro index 821c381..e054bb5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -133,7 +133,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all( </span> </div> </a> - <div class="sr-only"> + <div class="sr-only select-none"> <p class="p-summary" aria-label="Summary"> {entry.altText} </p> diff --git a/src/pages/licenses.toml.ts b/src/pages/licenses.toml.ts index 242fa52..ec21dbd 100644 --- a/src/pages/licenses.toml.ts +++ b/src/pages/licenses.toml.ts @@ -3,71 +3,88 @@ import { readFile } from "node:fs/promises"; 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. */ function verifyAttributions(licenses: string) { const { copyright, attributions } = parse(licenses); - // Make sure each copyright and attribution follows the TASL format. - // - T: title (or description) - // - A: author - // - S: source - // - L: license - // - other fields that have custom validation: type, notes, items + // Make sure each copyright and attribution follows the TASL format, + // and that other fields (type, notes, items) pass their custom validation. [copyright, attributions].flat().forEach((value) => { - // Title or description must be a valid string - const title = value.title ?? value.description; - if (typeof title !== "string" || !title) { - 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 TASL + validateTASL(value); + const title = copyright.title ?? copyright.description; // Validate extra optional fields // 1. Type must be a valid string if (typeof value.type !== "string") { @@ -80,13 +97,17 @@ function verifyAttributions(licenses: string) { if ("items" in value) { const items = value.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) => { 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 if ("notes" in value) { @@ -106,18 +127,33 @@ function verifyAttributions(licenses: string) { if ("additional" in copyright) { const additionals = copyright.additional; 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) => { if (typeof additional.notes !== "string" || !additional.notes) { 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 () => { const licenses = await readFile("./src/data/licenses.toml", { encoding: "utf-8" }); 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" } }); }; diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index 3fdb804..b8910c3 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -113,7 +113,7 @@ const totalPages = Math.ceil(page.total / page.size); </time> </div> </a> - <div class="sr-only"> + <div class="sr-only select-none"> <p class="p-category" aria-label="Category"> Story </p> diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index 4f9dea6..0a92916 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -177,7 +177,7 @@ const totalWorksWithTag = t( </time> </div> </a> - <div class="sr-only"> + <div class="sr-only select-none"> <p class="p-category" aria-label="Category"> Story </p> @@ -235,7 +235,7 @@ const totalWorksWithTag = t( </time> </div> </a> - <div class="sr-only"> + <div class="sr-only select-none"> <p class="p-category" aria-label="Category"> Game </p>