diff options
Diffstat (limited to 'src/components')
13 files changed, 587 insertions, 444 deletions
diff --git a/src/components/Citations.astro b/src/components/Citations.astro index ff42ad4..7ae6b26 100644 --- a/src/components/Citations.astro +++ b/src/components/Citations.astro @@ -1,9 +1,8 @@ --- -import type { CollectionEntry } from "astro:content"; -import { getEntries } from "astro:content"; +import type { BlogPosting } from "@lib/collection/types"; -type Props = { citations: CollectionEntry<"blog">["data"]["relatedPosts"] }; -const citations = await getEntries(Astro.props.citations ?? []); +type Props = { citations: BlogPosting[] }; +const { citations } = Astro.props; --- { citations.length > 0 && @@ -12,15 +11,15 @@ const citations = await getEntries(Astro.props.citations ?? []); <p>O autor recomenda ler também:</p> <ul> { - citations.map(({ collection, id, data }) => ( + citations.map(({ "@id": id, headline }) => ( <li itemprop="citation" itemscope itemtype="http://schema.org/BlogPosting" - itemid={Astro.url.href.replace(/[^\/]*\/?$/, id)} + itemid={id} > - <a href={`/${collection}/read/${id}`}> - <cite itemprop="headline">{data.title}</cite> + <a href={`/blog/read/${id}`}> + <cite itemprop="headline">{headline}</cite> </a> </li> )) diff --git a/src/components/Translations.astro b/src/components/Translations.astro index 6c2bf42..b78e5af 100644 --- a/src/components/Translations.astro +++ b/src/components/Translations.astro @@ -1,102 +1,115 @@ --- -import type { CollectionEntry } from "astro:content"; import { getFlagEmojiFromLocale, getLanguageNameFromLocale, } from "../utils/lang"; -import { getEntries } from "astro:content"; +import type { BlogPosting } from "@lib/collection/types"; +import { get } from "@utils/anonymous"; interface Props { - lang: string; - translations?: CollectionEntry<"blog">["data"]["translations"]; + id: string; + lang: Intl.LocalesArgument; + workTranslations: BlogPosting[]; + translationOfWork?: BlogPosting; } -const { lang } = Astro.props; +const { id, lang, workTranslations, translationOfWork } = Astro.props; -const translations = await getEntries(Astro.props.translations ?? []).then( - (translations) => - translations.sort((x, y) => x.data.lang.localeCompare(y.data.lang)), +const entries = [ + { "@type": "BlogPosting", "@id": id, inLanguage: lang }, + ...workTranslations, +]; +if (translationOfWork !== undefined) { + entries.push(translationOfWork); +} +const translations = entries.sort((x, y) => + x.inLanguage.localeCompare(y.inLanguage, lang) ); + +const list = new Intl.ListFormat(lang, { + type: "unit", + style: "narrow", +}); +const parts = list.formatToParts(translations.map(get("inLanguage"))); +let i = 0; --- -{ - /* TODO: What about <https://schema.org/translationOfWork> and <https://schema.org/translator>? */ -} +{/* TODO: <https://schema.org/translator> */} { translations.length > 0 && ( <aside> - <nav> - <p>Traduções:</p> - <ul class="translations"> + <nav class="mute small"> + <span>Traduções:{" "}</span> + <span class="translations" role="list"> { - translations.map(async ( - { data, collection, id }, - ) => { - const active = lang.localeCompare(data.lang) === 0; - return ( - <li - itemprop={active ? undefined : "workTranslation"} + parts.map( + ( + { type, value }: { + type: "element" | "literal"; + value: string; + }, + ) => { + switch (type) { + case "element": { + const { + "@id": identifier, + headline, + inLanguage, + } = translations[i++]; + const original = id === identifier; + const active = + lang.localeCompare(inLanguage, lang) === 0; + return ( + <span + role="listitem" + itemprop={active + ? undefined + : original + ? "translationOfWork" + : "workTranslation"} itemscope={!active} itemtype={active ? undefined : "http://schema.org/BlogPosting"} - itemid={active - ? undefined - : new URL(`${collection}/read/${id}`, Astro.site).href} + itemid={active ? undefined : identifier} > <a - href={`/${collection}/read/${id}`} + href={identifier} class:list={[{ active }]} rel={active ? undefined : "alternate"} - hreflang={active ? undefined : data.lang} + hreflang={active ? undefined : inLanguage} type="text/html" - title={data.title} - ><span class="emoji">{getFlagEmojiFromLocale(data.lang)}</span> - {getLanguageNameFromLocale(data.lang)} (<span + title={headline} + ><span class="emoji">{getFlagEmojiFromLocale(inLanguage)}</span> + {getLanguageNameFromLocale(inLanguage)} (<span itemprop="inLanguage" - >{data.lang}</span>)</a> - </li> + >{inLanguage}</span>)</a> + </span> ); - }) + } + case "literal": { + return <span>{value}</span>; + } + } + }, + ) } - </ul> + </span> </nav> </aside> ) } <style> - .translations { - list-style-type: none; - padding-inline-start: 0; - } - - .translations > li { - display: inline; + nav { + padding-block: calc(var(--size-2) * 1em); } - .translations > li > a > .emoji { - text-decoration: none; - font-family: var(--ff-icons); - } - - .translations > li > a.active { + a.active { font-weight: bolder; - text-decoration: underline; - color: var(--color-active); - } - - nav:has(.translations) { - display: flex; - gap: 1rem; - } - - nav:has(.translations) > * { - font-size: smaller; } - .translations > li:not(:first-child)::before { - content: "|"; - margin-inline: 0.5em; + a:hover { + color: var(--color-active); } @media print { diff --git a/src/components/licenses/WTFPL.astro b/src/components/licenses/WTFPL.astro deleted file mode 100644 index feab7ec..0000000 --- a/src/components/licenses/WTFPL.astro +++ /dev/null @@ -1,53 +0,0 @@ ---- -import type { Props as BaseProps } from "../CopyrightNotice.astro"; -interface Props extends BaseProps {} - -let { website, author, email, dateCreated } = Astro.props; ---- - -<footer itemprop="copyrightNotice"> - <p> - <small> - Copyright © <span itemprop="copyrightYear">{ - dateCreated.getFullYear() - }</span> - <span - itemprop="copyrightholder" - itemscope - itemtype="https://schema.org/Person" - >{ - website ? ( - <a - itemprop="url" - rel="author external noreferrer" - target="_blank" - href={website} - content={website} - ><span itemprop="name">{author}</span></a> - ) : author - } - { - email && ( - <><<a - itemprop="email" - rel="author external noreferrer" - target="_blank" - href={`mailto:${email}`} - >{email}</a>></> - ) - }</span> - </small> - </p> - <p> - <small> - This work is free. You can redistribute it and/or modify it under the - terms of the Do What The Fuck You Want To Public License, Version 2, as - published by Sam Hocevar. See <a - itemprop="license" - href="http://www.wtfpl.net/" - rel="license noreferrer" - target="_blank" - >http://www.wtfpl.net/</a> for more details. - </small> - </p> -</footer> diff --git a/src/components/organisms/Date.astro b/src/components/organisms/Date.astro index 960cfb7..3718664 100644 --- a/src/components/organisms/Date.astro +++ b/src/components/organisms/Date.astro @@ -3,9 +3,9 @@ import type { HTMLAttributes } from "astro/types"; interface Props { date: Date; - locales: Intl.LocalesArgument; + locales?: Intl.LocalesArgument; options: Intl.DateTimeFormatOptions; - itemprop: HTMLAttributes<"time">["itemprop"]; + itemprop?: HTMLAttributes<"time">["itemprop"]; } const { date, locales, options } = Astro.props; diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro deleted file mode 100644 index 4e52d4e..0000000 --- a/src/components/signature/Authors.astro +++ /dev/null @@ -1,275 +0,0 @@ ---- -import { toPK } from "@lib/pgp"; -import { createKeyFromArmor } from "@lib/pgp/create"; -import type { Verification } from "@lib/pgp/verify"; -import { defined, get, instanciate } from "@utils/anonymous"; -import { z } from "astro:content"; -import type { EntityTypesEnum } from "src/consts"; -import qrcode from "yaqrcode"; -import type { getSigners } from "@lib/collection/helpers"; - -interface Props { - verifications: NonNullable<Verification["verifications"]>; - expectedSigners: Awaited<ReturnType<typeof getSigners>>; - commitSignerKey?: string; -} - -const { - verifications: verificationsPromise, - expectedSigners, - commitSignerKey, -} = Astro.props; - -const fingerprintToData = new Map< - string, - { websites: URL[]; role: z.infer<typeof EntityTypesEnum> } ->(); - -for (const { entity, role } of expectedSigners) { - const key = await createKeyFromArmor(entity.data.publickey.armor); - const fingerprint = key.getFingerprint(); - fingerprintToData.set(fingerprint, { - websites: entity.data.websites?.map(instanciate(URL)) ?? [], - role, - }); -} - -let verifications = await Promise.all( - verificationsPromise.map(async ({ key, keyID, userID, verified }) => { - return { - key: await key, - keyID, - userID: await userID, - verified: await verified.catch(() => false), - }; - }), -); - -const expectedKeys = await Promise.all( - expectedSigners.map(get("entity")).map(({ data }) => - createKeyFromArmor(data.publickey.armor) - ), -); - -const expectedFingerprints = new Set( - expectedKeys.map((key) => key.getFingerprint()), -); - -const verifiedFingerprints = new Set( - verifications.map((v) => v.key).filter(defined).map(toPK).map((key) => - key.getFingerprint() - ), -); - -if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) { - throw new Error( - `Missing signature from expected signers: ${[ - ...expectedFingerprints.difference(verifiedFingerprints).values(), - ]}`, - ); -} ---- - -<div> - <table> - <caption> - <strong>Assinaturas</strong> - <p> - Para verificar uma assinatura é necessário a <a href="#message" - >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em - >chaves públicas</em> dos assinantes. Esta tabela mostra algumas - informações sobre os assinantes e as suas chaves públicas. - </p> - </caption> - <colgroup> - <col /> - <col /> - </colgroup> - <colgroup> - <col /> - <col /> - <col /> - </colgroup> - <thead> - <tr> - <th scope="col">Assinante</th> - <th scope="col">Função</th> - <th scope="col">Fingerprint</th> - <th scope="col">Válido</th> - <th scope="col">Commiter</th> - </tr> - </thead> - <tbody> - { - verifications.map(({ userID, key, keyID, verified }) => { - const fingerprint = key - ? toPK(key).getFingerprint() - : undefined; - const info = fingerprint - ? fingerprintToData.get(fingerprint) - : undefined; - const primary = userID?.[0]; - let role = ""; - switch (info?.role) { - case "author": { - role = "Autor"; - break; - } - case "co-author": { - role = "Co-autor"; - break; - } - case "translator": { - role = "Tradutor"; - break; - } - } - return ( - <tr> - <th scope="row"> - <address - itemprop="author" - itemscope - itemtype="https://schema.org/Person" - > - { - primary?.name - ? info?.websites[0] ? ( - <a - itemprop="url" - rel="author external noreferrer" - target="_blank" - href={info.websites[0]} - ><span itemprop="name">{primary.name}</span></a> - ) : <span itemprop="name">{primary.name}</span> - : primary?.email - ? ( - <><<a - itemprop="email" - rel="author external noreferrer" - target="_blank" - href={primary?.email && `mailto:${primary.email}`} - >{primary?.email}</a>></> - ) - : "" - } - { - primary && ( - <> - <button - popovertarget={`user-id-${fingerprint}`} - class="emoji" - > - ➕ - </button> - <section - class="user-id" - popover - id={`user-id-${fingerprint}`} - > - { - userID && ( - <><p><code>UserID</code>s</p><ul> - {userID.map((x) => <li>{x.userID}</li>)} - </ul></> - ) - } - { - info?.websites && ( - <><p>Websites</p><ul> - { - info.websites.map(( - x, - ) => <li><a href={x}>{x}</a></li>) - } - </ul></> - ) - } - </section> - </> - ) - } - </address> - </th> - <td>{role}</td> - <td> - <><span title={fingerprint?.replace(/(....)/g, "$1 ")}>{ - key - ? "0x" + toPK(key).getKeyID().toHex() - : "0x" + keyID.toHex() - }</span> - { - key && false && ( - <img - src={qrcode(toPK(key).armor(), { - typeNumber: 40, - errorCorrectLevel: "L", - })} - /> - ) - } - { - key && - ( - <button popovertarget={`armor-${fingerprint}`}> - Armor - </button> - <section class="armor" popover id={`armor-${fingerprint}`}> - <pre><code>{toPK(key).armor()}</code></pre> - </section> - ) - } - </> - </td> - <td>{verified ? "✅" : "❌"}</td> - <td> - { - commitSignerKey && - key?.getFingerprint().toUpperCase()?.endsWith( - commitSignerKey.toUpperCase(), - ) && "✅" - } - </td> - </tr> - ); - }) - } - </tbody> - </table> -</div> - -<style> - div { - overflow-x: auto; - } - - table { - table-layout: fixed; - border-collapse: collapse; - border: 3px solid; - margin-inline: auto; - max-width: 90svw; - } - - th, - td { - padding: 1rem; - text-align: center; - } - - tbody tr:nth-child(odd) { - background-color: #e7e7e7; - } - - section[popover] { - text-align: initial; - } - section[popover].armor { - max-height: calc(200dvh / 3); - } - @media (prefers-color-scheme: dark) { - tbody tr:nth-child(odd) { - background-color: #181818; - } - } -</style> diff --git a/src/components/templates/Authors.astro b/src/components/templates/Authors.astro new file mode 100644 index 0000000..61ca026 --- /dev/null +++ b/src/components/templates/Authors.astro @@ -0,0 +1,355 @@ +--- +import { type RevocationReason, toPK } from "@lib/pgp"; +import type { Verification } from "@lib/pgp/verify"; +import { defined, get } from "@utils/anonymous"; +import type { getSigners } from "@lib/collection/helpers"; +import type { PublicKey, UserIDPacket } from "openpgp"; +import { + createVerificationSummary, + VerificationResult, +} from "@lib/pgp/summary"; +import Date from "@components/organisms/Date.astro"; + +interface Props { + verifications: NonNullable<Verification["verifications"]>; + expectedSigners: Map< + string, + { + signer: Awaited<ReturnType<typeof getSigners>>[number]; + users: UserIDPacket[]; + key: PublicKey; + } + >; + commitSignerKey?: string; +} + +const { + verifications: verificationsPromise, + expectedSigners, + commitSignerKey, +} = Astro.props; + +let verifications = await Promise.all( + verificationsPromise.map(async (verification) => { + const { key, keyID, userID, verified } = verification; + return { + key: await key, + keyID, + userID: await userID, + verified: await verified.catch(() => false), + summary: await createVerificationSummary(verification), + }; + }), +); + +const expectedKeys = Array.from(expectedSigners.values()).map(get("key")); + +const expectedFingerprints = new Set( + expectedKeys.map((key) => key.getFingerprint()), +); + +const verifiedFingerprints = new Set( + verifications.map((v) => v.key).filter(defined).map(toPK).map((key) => + key.getFingerprint() + ), +); + +if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) { + throw new Error( + `Missing signature from expected signers: ${[ + ...expectedFingerprints.difference(verifiedFingerprints).values(), + ]}`, + ); +} +--- + +<div> + <table> + <caption> + <strong>Assinaturas</strong> + <p> + Para verificar uma assinatura é necessário a <a href="#message" + >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em + >chaves públicas</em> dos assinantes. Esta tabela mostra algumas + informações sobre os assinantes, as suas chaves públicas e as suas + assinaturas. + </p> + </caption> + <colgroup> + <col /> + <col /> + </colgroup> + <colgroup> + <col /> + <col /> + <col /> + </colgroup> + <colgroup> + <col /> + </colgroup> + <thead> + <tr> + <th scope="col">Assinante</th> + <th scope="col">Função</th> + <th scope="col">Fingerprint</th> + <th scope="col">Válido</th> + <th scope="col">Commiter</th> + <th scope="col">Mais Informações</th> + </tr> + </thead> + <tbody> + { + verifications.map( + ({ userID, key, keyID, verified, summary }) => { + const fingerprint = key + ? toPK(key).getFingerprint() + : undefined; + const info = fingerprint + ? expectedSigners.get(fingerprint) + : undefined; + const primary = userID?.[0]; + const signer = info?.signer; + let role = ""; + switch (signer?.role) { + case "author": { + role = "Autor"; + break; + } + case "co-author": { + role = "Co-autor"; + break; + } + case "translator": { + role = "Tradutor"; + break; + } + } + const { + reasons, + created, + expired, + revoked, + revocationReason, + trusted, + } = (summary[1].get(keyID.toHex()) ?? []).reduce( + (acc, x) => { + if (!("key" in x || "keyID" in x)) { + return acc; + } + + switch (x.result) { + case VerificationResult.MISSING_KEY: + acc.reasons.push(x.reason); + acc.created = x.created ?? undefined; + break; + case VerificationResult.UNTRUSTED_KEY: + acc.created = x.created ?? undefined; + acc.trusted &&= false; + break; + case VerificationResult.TRUSTED_KEY: + acc.created = x.created ?? undefined; + acc.trusted = true; + break; + case VerificationResult + .EXPIRATION_AFTER_SIGNATURE: + acc.created = x.created ?? undefined; + acc.expired = x.expired; + break; + case VerificationResult + .EXPIRATION_BEFORE_SIGNATURE: + acc.created = x.created ?? undefined; + acc.expired = x.expired; + break; + case VerificationResult + .REVOCATION_AFTER_SIGNATURE: + acc.created = x.created ?? undefined; + acc.revoked = x.revoked; + acc.revocationReason = x.revocationReason; + break; + case VerificationResult + .REVOCATION_BEFORE_SIGNATURE: + acc.created = x.created ?? undefined; + acc.revoked = x.revoked; + acc.revocationReason = x.revocationReason; + break; + case VerificationResult.KEY_DOES_NOT_SIGN: + break; + } + + return acc; + }, + { + reasons: [], + created: undefined, + expired: undefined, + revoked: undefined, + revocationReason: undefined, + trusted: false, + } as { + reasons: Error[]; + created?: Date; + expired?: Date; + revoked?: Date; + revocationReason?: RevocationReason; + trusted: boolean; + }, + ); + return ( + <tr> + <th scope="row"> + <address + itemprop="author" + itemscope + itemtype="https://schema.org/Person" + > + { + primary?.name + ? signer?.entity?.data?.websites[0] + ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={signer?.entity?.data?.websites[0]} + ><span itemprop="name">{primary.name}</span></a> + ) + : <span itemprop="name">{primary.name}</span> + : primary?.email + ? ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={primary?.email && `mailto:${primary.email}`} + >{primary?.email}</a>></> + ) + : "" + } + </address> + {signer !== undefined && <><hr /><a href="#">Ver perfil</a></>} + </th> + <td>{role}</td> + <td> + <samp title={fingerprint?.replace(/(....)/g, "$1 ")}>{ + key + ? "0x" + toPK(key).getKeyID().toHex() + : "0x" + keyID.toHex() + }</samp> + </td> + <td>{verified ? "✅" : "❌"}</td> + <td> + { + commitSignerKey && + key?.getFingerprint().toUpperCase()?.endsWith( + commitSignerKey.toUpperCase(), + ) && "✅" + } + </td> + <td> + { + key && ( + <> + <button + type="button" + class="emoji" + popovertarget={`info-${fingerprint}`} + > + ➕ + </button> + <section popover id={`info-${fingerprint}`}> + <p>Erros</p> + { + summary[0].map((x) => { + if (!("reason" in x)) { + return undefined; + } + + let reason = x.reason; + + return <pre>{reason.message}</pre>; + }) + } + <p><strong>Informações</strong></p> + <dl class="divider"> + { + reasons.map(({ message }) => ( + <> <dt>Erro</dt> <dd>{message}</dd></> + )) + } + { + created && ( + <> + <dt>Data da assinatura</dt> + <dd><Date date={created} options={{}} /></dd> + </> + ) + } + { + expired && ( + <> + <dt>Data de expiração</dt> + <dd><Date date={expired} options={{}} /></dd> + </> + ) + } + { + revoked && ( + <> + <dt>Data de revogação</dt> + <dd><Date date={revoked} options={{}} /></dd> + </> + ) + } + { + revocationReason && ( + <> + <dt>Razão para a revogação</dt> + <dd> + {revocationReason.flag}: {revocationReason.msg} + </dd> + </> + ) + } + <dt>Chave confiável</dt> + <dd>{trusted ? "✅" : "❌"}</dd> + </dl> + </section></> + ) + } + </td> + </tr> + ); + }, + ) + } + </tbody> + </table> +</div> + +<style> + div { + overflow-x: auto; + } + + table { + table-layout: fixed; + border-collapse: collapse; + border: 3px solid; + margin-inline: auto; + max-width: 90svw; + } + + th, + td { + padding: 1rem; + text-align: center; + } + + tbody tr:nth-child(odd) { + background-color: oklch(from var(--color-light) l c h / calc(alpha * 0.25)); + } + + section[popover] { + text-align: initial; + } +</style> diff --git a/src/components/CopyrightNotice.astro b/src/components/templates/CopyrightNotice.astro index 8a5ebc6..8335293 100644 --- a/src/components/CopyrightNotice.astro +++ b/src/components/templates/CopyrightNotice.astro @@ -5,25 +5,26 @@ import { } from "@lib/collection/schemas"; import CC from "./licenses/CC.astro"; import WTFPL from "./licenses/WTFPL.astro"; +import type { Person } from "@lib/collection/types"; export interface Props { title: string; - author: string; - email?: string; - website?: string; - dateCreated: Date; + holders: Person[]; + years: number[]; license?: typeof LICENSES[number]; } let { license = "public domain" } = Astro.props; let Notice = undefined; -if (license === "WTFPL") { - Notice = WTFPL; -} else if ( - CREATIVE_COMMONS_LICENSES.some((x) => license.localeCompare(x) === 0) -) { - Notice = CC; +if (license !== undefined) { + if (license === "WTFPL") { + Notice = WTFPL; + } else if ( + CREATIVE_COMMONS_LICENSES.some((x) => license.localeCompare(x) === 0) + ) { + Notice = CC; + } } --- diff --git a/src/components/licenses/CC.astro b/src/components/templates/licenses/CC.astro index 61f9114..2aea423 100644 --- a/src/components/licenses/CC.astro +++ b/src/components/templates/licenses/CC.astro @@ -1,8 +1,13 @@ --- -import type { Props as BaseProps } from "../CopyRightNotice.astro"; -interface Props extends BaseProps {} +import type { ComponentProps } from "astro/types"; +import type CopyrightNotice from "@components/templates/CopyrightNotice.astro"; +import { listYearsWithRanges } from "@utils/datetime"; +interface Props extends ComponentProps<typeof CopyrightNotice> {} + +let { license, title, holders, years } = Astro.props; + +if (typeof license !== "string") throw new Error(); -let { title, website, author, dateCreated, license } = Astro.props; const publicdomain = license === "CC0"; const sa = /SA/.test(license); const nd = /ND/.test(license); @@ -10,6 +15,9 @@ const nc = /NC/.test(license); const licenseURL = `https://creativecommons.org/licenses/${ license.slice(3).toLowerCase() }/4.0/`; + +const firstYear = Math.min(...years); +const lastYears = years.sort((a, b) => a - b).slice(1); --- <footer itemprop="copyrightNotice"> @@ -17,21 +25,31 @@ const licenseURL = `https://creativecommons.org/licenses/${ publicdomain ? ( <p> <small> - <a href={Astro.url}>{title}</a> by <span - itemprop="copyrightholder" - itemscope - itemtype="https://schema.org/Person" - >{ - website ? ( - <a - itemprop="url" - rel="author external noreferrer" - target="_blank" - href={website} - content={website} - ><span itemprop="name">{author}</span></a> - ) : author - }</span> is marked <a + <a href={Astro.url}>{title}</a> by { + holders.map(({ name, url }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + }</span> + ); + }) + } is marked <a itemprop="license" rel="license noreferrer" target="_blank" @@ -54,22 +72,37 @@ const licenseURL = `https://creativecommons.org/licenses/${ <p> <small> <a href={Astro.url}>{title}</a> © <span itemprop="copyrightYear">{ - dateCreated.getFullYear() - }</span> by <span - itemprop="copyrightholder" - itemscope - itemtype="https://schema.org/Person" - >{ - website ? ( - <a - itemprop="url" - href={website} - target="_blank" - rel="author external noreferrer" - content={website} - ><span itemprop="name">{author}</span></a> - ) : author - }</span> is licensed under <a + firstYear + }</span>, { + listYearsWithRanges(lastYears, { + list: { type: "unit", style: "narrow" }, + locale: "en", + }).replace(/^\d+/, "") + } by { + holders.map(({ name, url }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + }</span> + ); + }) + } is licensed under <a itemprop="license" rel="license noreferrer" target="_blank" diff --git a/src/components/templates/licenses/WTFPL.astro b/src/components/templates/licenses/WTFPL.astro new file mode 100644 index 0000000..d3546c7 --- /dev/null +++ b/src/components/templates/licenses/WTFPL.astro @@ -0,0 +1,70 @@ +--- +import { listYearsWithRanges } from "@utils/datetime"; +import type { Props as BaseProps } from "../CopyrightNotice.astro"; +interface Props extends BaseProps {} + +const { years, holders } = Astro.props; +const firstYear = Math.min(...years); +const lastYears = years.sort((a, b) => a - b).slice(1); +--- + +<footer itemprop="copyrightNotice"> + <p> + <small> + Copyright © <span itemprop="copyrightYear">{firstYear}</span>, { + listYearsWithRanges(lastYears, { + list: { type: "unit", style: "narrow" }, + locale: "en", + }).replace(/^\d+/, "") + } + + { + holders.map(({ name, url, email }) => { + if (name === undefined) return undefined; + + const website = url?.[0]; + + return ( + <span + itemprop="copyrightHolder" + itemscope + itemtype="https://schema.org/Person" + >{ + website !== undefined ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={website} + content={website} + ><span itemprop="name">{name}</span></a> + ) : <span itemprop="name">{name}</span> + } + { + email !== undefined && ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={`mailto:${email}`} + >{email}</a>></> + ) + }</span> + ); + }) + } + </small> + </p> + <p> + <small> + This work is free. You can redistribute it and/or modify it under the + terms of the Do What The Fuck You Want To Public License, Version 2, as + published by Sam Hocevar. See <a + itemprop="license" + href="http://www.wtfpl.net/" + rel="license noreferrer" + target="_blank" + >http://www.wtfpl.net/</a> for more details. + </small> + </p> +</footer> diff --git a/src/components/signature/Commit.astro b/src/components/templates/signature/Commit.astro index 328a8f9..328a8f9 100644 --- a/src/components/signature/Commit.astro +++ b/src/components/templates/signature/Commit.astro diff --git a/src/components/signature/Downloads.astro b/src/components/templates/signature/Downloads.astro index 3497b37..3497b37 100644 --- a/src/components/signature/Downloads.astro +++ b/src/components/templates/signature/Downloads.astro diff --git a/src/components/signature/Signature.astro b/src/components/templates/signature/Signature.astro index 57e9902..57e9902 100644 --- a/src/components/signature/Signature.astro +++ b/src/components/templates/signature/Signature.astro diff --git a/src/components/signature/Summary.astro b/src/components/templates/signature/Summary.astro index f25c1d1..3469a0f 100644 --- a/src/components/signature/Summary.astro +++ b/src/components/templates/signature/Summary.astro @@ -1,6 +1,6 @@ --- import { - createVerificationSummary, + createVerificationsSummary, logLevel, type Summary, VerificationResult, @@ -11,7 +11,7 @@ import type { NonEmptyArray } from "@utils/iterator"; interface Props extends Verification {} -let [errors, keys] = await createVerificationSummary(Astro.props); +let [errors, keys] = await createVerificationsSummary(Astro.props); const failed = errors.filter((summary) => "reason" in summary); if (failed.length > 0) { |