diff options
Diffstat (limited to 'src/pages/blog')
-rw-r--r-- | src/pages/blog/read/[...slug].astro | 449 | ||||
-rw-r--r-- | src/pages/blog/read/[slug].astro | 576 |
2 files changed, 576 insertions, 449 deletions
diff --git a/src/pages/blog/read/[...slug].astro b/src/pages/blog/read/[...slug].astro deleted file mode 100644 index 71d0929..0000000 --- a/src/pages/blog/read/[...slug].astro +++ /dev/null @@ -1,449 +0,0 @@ ---- -import { type CollectionEntry, getCollection } from "astro:content"; -import { render } from "astro:content"; -import Translations from "@components/Translations.astro"; -import { toIso8601Full } from "@utils/datetime"; -import ReadingTime from "@components/ReadingTime.astro"; -import Keywords from "@components/Keywords.astro"; -import Citations from "@components/Citations.astro"; -import Signature from "@components/signature/Signature.astro"; -import CopyrightNotice from "@components/CopyrightNotice.astro"; -import { verifier as verifierPrototype } from "@lib/pgp/verify"; -import { getSigners, isTranslation } from "@lib/collection/helpers"; -import { get } from "@utils/anonymous"; -import Authors from "@components/signature/Authors.astro"; -import { getEntry } from "astro:content"; -import Base from "@layouts/Base.astro"; -import readingTime from "reading-time"; -import type { - Entry, - MicroEntry, - OriginalEntry, -} from "@lib/collection/schemas"; - -export async function getStaticPaths() { - const posts = await getCollection("blog"); - return posts.map((post) => ({ - params: { slug: post.id }, - props: post, - })); -} - -type Props = CollectionEntry<"blog">; - -const post = Astro.props; - -let original: OriginalEntry | MicroEntry; -if (isTranslation(post)) { - original = await getEntry(post.data.translationOf) as - | OriginalEntry - | MicroEntry; - - if (!original) { - throw new Error(`Original post not found for ${post.id}`); - } - - const originalAuthor = (original.data.signers ?? []).filter( - (s) => s.role === "author", - ).map((s) => s.entity.id)?.[0]; - const originalCoAuthors = new Set( - (original.data.signers ?? []).filter( - (s) => s.role === "co-author", - ).map((s) => s.entity.id), - ); - const translationAuthor = (post.data.signers ?? []).filter( - (s) => s.role === "author", - ).map((s) => s.entity.id)?.[0]; - const translationCoAuthors = new Set( - (post.data.signers ?? []).filter( - (s) => s.role === "co-author", - ).map((s) => s.entity.id), - ); - - if ( - (translationAuthor !== undefined && - translationAuthor !== originalAuthor) || - !translationCoAuthors.isSubsetOf(originalCoAuthors) - ) { - throw new Error( - `Post ${post.id} has mismatched (co-)authors from original post ${original.id}`, - ); - } - - const translators = (post.data.signers ?? []).filter( - (s) => s.role === "translator", - ).map((s) => s.entity.id); - - for (const translator of translators) { - if ( - originalAuthor === translator || originalCoAuthors.has(translator) - ) { - throw new Error( - `Translator ${translator} in ${post.id} is already a (co-)author in original post`, - ); - } - } -} else { - original = post; - if (post.data.signers?.some((x) => x.role === "translator")) { - throw new Error( - `Post ${post.id} is not a translation but has translators defined`, - ); - } -} - -// Add own post as a translation -const translationsSet = new Set( - (await getCollection( - "blog", - (x) => - (x.data.kind === "translation") && x.data.translationOf.id === - (post.data.kind === "translation" - ? post.data.translationOf.id - : post.id), - ) ?? []).map(({ id }) => id), -); - -translationsSet.add(post.id); -const translations = [...translationsSet.values()].map((id) => ({ - collection: post.collection, - id, -})); - -const signers = await getSigners(post); - -const verifier = await verifierPrototype.then((x) => x.clone()); - -// Add signers public keys to keyring -for (const { data } of signers.map(get("entity"))) { - if (data.publickey.armor !== undefined) { - verifier.addKeyFromArmor(data.publickey.armor); - } -} - -const verification = post.filePath !== undefined - ? await verifier.verify([ - new URL(`file://${Deno.cwd()}/${post.filePath}`), - ]) - : undefined; - -const { Content } = await render(post); - -const { lang } = post.data; - -const commit = await verification?.commit; - -const reading = post.body ? readingTime(post.body, {}) : undefined; -const minutes = reading === undefined - ? undefined - : Math.ceil(reading.minutes); -const estimative = reading === undefined - ? undefined - : new Intl.DurationFormat(lang, { - style: "long", - }).format({ minutes }); -const duration = minutes === undefined - ? undefined - : `PT${Math.floor(minutes / 60) > 0 ? Math.floor(minutes / 60) + "H" : ""}${ - minutes % 60 > 0 ? minutes % 60 + "M" : "" - }`; - -const getOrUndefined = (k: string) => - k in post.data ? post.data[k as keyof typeof post.data] : undefined; -const author = { - "@type": "Person", -} as const; -const contributor = post.data.signers.filter(({ role }) => - role === "co-author" -).map(() => { - return { - "@type": "Person", - } as const; -}); -const translator = post.data.signers.filter(({ role }) => - role === "translator" -).map(() => { - return { - "@type": "Person", - } as const; -}); -const JSONLD = { - "@context": "https://schema.org", - "@type": "BlogPosting", - "@id": Astro.url.href, - articleBody: post.rendered?.html ?? post.body, - abstract: getOrUndefined("description"), - alternativeHeadline: getOrUndefined("subtitle"), - author, - citation: [].map(() => { - return { - "@type": "CreativeWork", - }; - }), - contributor, - copyrightHolder: [author, ...contributor, ...translator], - // copyrightNotice: post.data.license, // WORKAROUND - copyrightYear: post.data.dateCreated.getFullYear(), - creativeWorkStatus: "Published", - dateCreated: post.data.dateCreated.toISOString(), - dateModified: "dateUpdated" in post.data - ? post.data.dateUpdated?.toISOString() - : undefined, - // datePublished: undefined, // from git commit commit date - encodingFormat: "text/html", - headline: post.data.title, - inLanguage: post.data.lang, - isAccessibleForFree: true, - isBasedOn: isTranslation(post) - ? { - "@type": "BlogPosting", - "@id": new URL(`blog/read/${post.data.translationOf}`, Astro.site).href, - } - : undefined, - keywords: original.data.keywords, - license: post.data.license, // WORKAROUND - locationCreated: { - "@type": "Place", - // XXX: getOrUndefined("locationCreated"), - }, - mentions: [].map(() => { - return { - "@type": "Thing", - }; - }), - // publication: { - // "@type": "PublicationEvent", - // }, // from git commit - // publisher: { - // "@type": "Person", - // }, // from git commit - text: post.rendered?.html ?? post.body, - timeRequired: post.body !== undefined ? duration : undefined, - translationOf: isTranslation(post) - ? { - "@type": "BlogPosting", - "@id": new URL(`blog/read/${post.data.translationOf}`, Astro.site).href, - } - : undefined, - translator, - // version: undefined // TODO - wordCount: reading?.words, - workTranslations: translations.filter(({ id }) => id !== post.id).map(( - { id }, - ) => ({ - "@type": "BlogPosting", - "@id": new URL(`blog/read/${id}`, Astro.site).href, - })), - description: getOrUndefined("description"), - name: post.data.title, - url: Astro.url.href, -} as const; ---- - -<Base - title={post.data.title} - description={"description" in post.data ? post.data.description : post.data.title} -> - <main> - <article - itemscope - itemtype="http://schema.org/BlogPosting" - itemid={Astro.url.href} - > - <Translations {translations} {lang} /> - <hgroup> - <h1 itemprop="headline">{post.data.title}</h1> - { - "subtitle" in post.data && ( - <p itemprop="alternativeHeadline" class="subtitle"> - {post.data.subtitle} - </p> - ) - } - </hgroup> - { - "description" in post.data && post.data.description && - ( - <section itemprop="abstract"> - <h2>Resumo</h2> - { - post.data.description.split(new RegExp("\\s{2,}")) - .map(( - x, - ) => <p>{x}</p>) - } - </section> - ) - } - {verification && <Signature {lang} {verification} />} - <footer> - { - verification?.verifications && - ( - <Authors - verifications={verification.verifications} - expectedSigners={signers} - commitSignerKey={commit?.signature?.signer} - /> - ) - } - <dl> - <dt>Data de criação</dt> - <dd> - <time - itemprop="dateCreated" - datetime={toIso8601Full(post.data.dateCreated)} - >{ - new Intl.DateTimeFormat([lang], {}).format( - post.data.dateCreated, - ) - }</time> - </dd> - { - post.data.dateUpdated && ( - <dt>Última atualização</dt><dd> - <time - itemprop="dateModified" - datetime={toIso8601Full(post.data.dateUpdated)} - >{ - new Intl.DateTimeFormat([lang], {}).format( - post.data.dateUpdated, - ) - }</time> - </dd> - ) - } - { - "locationCreated" in post.data && - post.data.locationCreated && ( - <dt - itemprop="locationCreated" - itemscope - itemtype="https://schema.org/Place" - > - Local de criação - </dt><dd> - <span itemprop="name">{post.data.locationCreated}</span> - </dd> - ) - } - </dl> - <ReadingTime body={post.body} {lang} /> - </footer> - <hr /> - <div itemprop="articleBody text"><Content /></div> - <hr /> - { - "keywords" in original.data && ( - <Keywords keywords={original.data.keywords} /> - ) - } - { - "relatedPosts" in original.data && ( - <Citations citations={original.data.relatedPosts} /> - ) - } - <CopyrightNotice - author={signers[0]?.entity.data.websites?.[0] ?? "Anonymous"} - website={signers[0]?.entity.data.websites?.[0]} - email={signers[0]?.entity.data.websites?.[0]} - title={post.data.title} - dateCreated={post.data.dateCreated} - license={post.data.license} - /> - </article> - </main> -</Base> - -<script - type="application/ld+json" - is:inline - set:html={JSON.stringify(JSONLD)} -/> - -<script type="module" is:inline> - hashchange(); - - window.addEventListener("hashchange", hashchange); - - document.addEventListener( - "click", - function (event) { - if ( - event.target && - event.target instanceof HTMLAnchorElement && - event.target.href === location.href && - location.hash.length > 1 - ) { - requestIdleCallback(function () { - if (!event.defaultPrevented) { - hashchange(); - } - }); - } - }, - false, - ); - - function hashchange() { - let hash; - - try { - hash = decodeURIComponent(location.hash.slice(1)).toLowerCase(); - } catch (e) { - return; - } - - const name = "user-content-" + hash; - const target = document.getElementById(name) || - document.getElementsByName(name)[0]; - - if (target) { - requestIdleCallback(function () { - target.scrollIntoView(); - }); - } - } -</script> - -<style is:inline> - section[data-footnotes].footnotes { - word-wrap: break-word; - } -</style> - -<style> - hgroup { - text-align: center; - } - - .subtitle { - font-weight: lighter; - } - - [itemprop~="articleBody"] { - line-height: 1.4; - font-size: 1.2em; - text-align: justify; - - & h1, - & h2, - & h3 { - line-height: 1.2; - } - } - - [itemprop="abstract"] { - margin-inline: 1em; - padding-block: 1em; - font-style: italic; - } - - @media print { - body { - font-size: 1rem; - font-family: var(--ff-serif); - line-height: 1.62; - } - } -</style> diff --git a/src/pages/blog/read/[slug].astro b/src/pages/blog/read/[slug].astro new file mode 100644 index 0000000..263b31d --- /dev/null +++ b/src/pages/blog/read/[slug].astro @@ -0,0 +1,576 @@ +--- +import { type CollectionEntry, getCollection } from "astro:content"; +import { render } from "astro:content"; +import Translations from "@components/Translations.astro"; +import KeywordsList from "@components/organisms/KeywordsList.astro"; +import Citations from "@components/Citations.astro"; +import Signature from "@components/templates/signature/Signature.astro"; +import CopyrightNotice from "@components/templates/CopyrightNotice.astro"; +import { verifier as verifierPrototype } from "@lib/pgp/verify"; +import { + fromPosts, + getSigners, + getSignersIDs, + isTranslation, + licenseNotice, + licenseURL, +} from "@lib/collection/helpers"; +import { defined, get, transform } from "@utils/anonymous"; +import Authors from "@components/templates/Authors.astro"; +import Base from "@layouts/Base.astro"; +import type { + GetStaticPaths, + InferGetStaticParamsType, + InferGetStaticPropsType, +} from "astro"; +import DateTime from "@components/organisms/Date.astro"; +import { getUserIDsFromKey } from "@lib/pgp/user"; +import type { PublicKey, UserIDPacket } from "openpgp"; +import type { BlogPosting, Person } from "@lib/collection/types"; +import { + type MicroEntry, + Original, + type OriginalEntry, + Translation, +} from "@lib/collection/schemas"; +import { getEntry } from "astro:content"; +import { getEntries } from "astro:content"; +import readingTime from "reading-time"; +import { fileCreationCommitDate } from "@lib/git/log"; + +export const getStaticPaths = (async (): Promise< + { + params: { slug: string }; + props: CollectionEntry<"blog">; + }[] +> => { + const posts = await getCollection("blog"); + return posts.map((post) => ({ + params: { slug: post.id }, + props: post, + })); +}) satisfies GetStaticPaths; + +type Params = InferGetStaticParamsType<typeof getStaticPaths>; +type Props = InferGetStaticPropsType<typeof getStaticPaths>; + +let post: Props | undefined = Astro.props; + +const verifier = await verifierPrototype.then((x) => x.clone()); + +const signers: Map< + string, + { + signer: Awaited<ReturnType<typeof getSigners>>[number]; + users: UserIDPacket[]; + key: PublicKey; + } +> = new Map(); +// Add signers public keys to keyring +for (const signer of await getSigners(post)) { + const { data } = signer.entity; + const key = await verifier.addKeyFromArmor(data.publickey.armor); + signers.set(key.getFingerprint(), { + signer, + users: getUserIDsFromKey(undefined, key), + key, + }); +} + +const createPerson = ( + { signer, users }: typeof signers extends Map<any, infer V> ? V : never, +): Person | undefined => ({ + "@type": "Person", + "@id": signer.entity.id, // TODO: URL + name: users.find(({ name }) => name.length > 0)?.name, + url: signer.entity.data.websites, + email: users.find(({ email }) => email.length > 0)?.email, +}); + +const signersValues = Array.from(signers.values()); +const author: Person | undefined = transform( + signersValues.find(({ signer }) => signer.role === "author"), + (x) => x !== undefined ? createPerson(x) : undefined, +); +const coauthors: Person[] = signersValues.filter(({ signer }) => + signer.role === "co-author" +).map(createPerson).filter(defined); +const translators: Person[] = signersValues.filter(({ signer }) => + signer.role === "translator" +).map(createPerson).filter(defined); + +const { id, data, rendered, body, filePath } = post; + +const path = new URL(`file://${Deno.cwd()}/${filePath}`); +const verification = post.filePath !== undefined + ? await verifier.verify([path]) + : undefined; + +const commit = await verification?.commit; + +const { title, lang, dateCreated, dateUpdated, license } = data; + +let original: OriginalEntry | MicroEntry; +try { + const { translationOf } = Translation.parse(post); + const maybeOriginal = await getEntry(translationOf) as + | OriginalEntry + | MicroEntry + | undefined; + + if (maybeOriginal === undefined) { + throw new Error(`Original post not found for ${id}`); + } + + original = maybeOriginal; + + const { author: [originalAuthors], "co-author": originalCoauthors } = + getSignersIDs(original); + const originalAuthor = originalAuthors?.[0]; + + if ( + (author !== undefined && + author["@id"] !== originalAuthor) || + !new Set(coauthors).isSubsetOf(new Set(originalCoauthors)) + ) { + throw new Error( + `Post ${id} has mismatched (co-)authors from original post ${original.id}`, + ); + } + + for (const { "@id": t } of translators) { + if ( + originalAuthor === t || originalCoauthors.includes(t) + ) { + throw new Error( + `Translator ${t} in ${id} is already a (co-)author in original post`, + ); + } + } +} catch { + original = post as OriginalEntry | MicroEntry; + if (signersValues.some(({ signer }) => signer.role === "translator")) { + throw new Error( + `Post ${id} is not a translation but has translators defined`, + ); + } +} + +const translationsSet = await fromPosts( + isTranslation, + (x) => + new Set( + x.filter(({ data }) => data.translationOf.id === original.id).map( + get("id"), + ), + ), +); +translationsSet.add(original.id); + +const translations = await getEntries( + Array.from(translationsSet).map((id) => ({ + collection: original.collection, + id, + })), +); + +const reading = body ? readingTime(body, {}) : undefined; +const minutes = reading === undefined + ? undefined + : Math.ceil(reading.minutes); +const estimative = minutes === undefined + ? undefined + : new Intl.DurationFormat(lang, { + style: "long", + }).format({ hours: Math.floor(minutes / 60), minutes: minutes % 60 }); +const duration = minutes === undefined + ? undefined + : `PT${Math.floor(minutes / 60) > 0 ? Math.floor(minutes / 60) + "H" : ""}${ + minutes % 60 > 0 ? minutes % 60 + "M" : "" + }`; + +const linkedData: BlogPosting & { "@context": "https://schema.org" } = { + "@context": "https://schema.org", + "@type": "BlogPosting", + "@id": Astro.url.href, + url: Astro.url.href, + headline: title, + name: title, + abstract: "description" in data ? data.description : undefined, + alternativeHeadline: "subtitle" in data ? data.subtitle : undefined, + inLanguage: lang, + workTranslations: translations.filter((post) => + post.id !== id && post.id !== original.id + ).map(({ id, data }) => + ({ + "@type": "BlogPosting", + "@id": new URL(`blog/read/${id}`, Astro.site).href, + url: new URL(`blog/read/${id}`, Astro.site).href, + headline: data.title, + name: data.title, + inLanguage: data.lang, + dateCreated: data.dateCreated.toISOString(), + license: licenseURL(data.license)?.href, + translator: data.signers.filter(({ role }) => role === "translator") + .map(( + { entity }, + ): Person => ({ + "@type": "Person", + "@id": entity.id, + })), + }) as BlogPosting + ), + translationOfWork: original.id !== post.id + ? { + "@type": "BlogPosting", + "@id": new URL(`blog/read/${original.id}`, Astro.site).href, + url: new URL(`blog/read/${original.id}`, Astro.site).href, + headline: original.data.title, + name: original.data.title, + inLanguage: original.data.lang as string, + dateCreated: original.data.dateCreated.toISOString(), + license: licenseURL(original.data.license)?.href, + } as BlogPosting + : undefined, + // TODO: version + author, + contributor: coauthors, + translator: translators, + dateCreated: dateCreated.toISOString(), + dateModified: dateUpdated?.toISOString(), + datePublished: await fileCreationCommitDate(path).then((date) => + date?.toISOString() + ), + timeRequired: duration, + wordCount: reading?.words, + articleBody: rendered?.html ?? body, + text: rendered?.html ?? body, + keywords: original.data.keywords, + citation: await transform( + Original.safeParse(original.data).data, + async (o) => { + if (o === undefined) return o; + const related = await getEntries(o.relatedPosts); + return related.map(({ data }): BlogPosting => ({ + "@type": "BlogPosting", + "@id": new URL(`blog/read/${id}`, Astro.site).href, + url: new URL(`blog/read/${id}`, Astro.site).href, + headline: data.title, + name: data.title, + inLanguage: data.lang, + dateCreated: data.dateCreated.toISOString(), + license: licenseURL(data.license)?.href ?? undefined, + })); + }, + ), // TODO: citation V.S. mentions + mentions: await transform( + Original.safeParse(original.data).data, + async (o) => { + if (o === undefined) return o; + const related = await getEntries(o.relatedPosts); + return related.map(({ data }): BlogPosting => ({ + "@type": "BlogPosting", + "@id": new URL(`blog/read/${id}`, Astro.site).href, + url: new URL(`blog/read/${id}`, Astro.site).href, + headline: data.title, + name: data.title, + inLanguage: data.lang, + dateCreated: data.dateCreated.toISOString(), + license: licenseURL(data.license)?.href ?? undefined, + })); + }, + ), // TODO: citation V.S. mentions + copyrightHolder: [author, ...coauthors, ...translators].filter(defined), + copyrightNotice: licenseNotice(license, { + title, + holders: signersValues.map(({ users }) => { + const user = users?.[0]; + if (user === undefined) return undefined; + + const { name, email } = user; + + return (name.length > 0 && email.length > 0) + ? { name, email } + : undefined; + }).filter(defined), + years: new Array( // TODO: get years where there were commits + (dateUpdated?.getFullYear() ?? dateCreated.getFullYear()) - + dateCreated.getFullYear() + 1, + ).fill(dateCreated.getFullYear()).map((x, i) => x + i), + }, lang), + copyrightYear: dateCreated.getFullYear(), + creativeWorkStatus: "Published", + encodingFormat: "text/html", + isAccessibleForFree: true, + license: licenseURL(license)?.href ?? undefined, + publisher: transform(commit?.committer, (commiter) => { + if (commiter === undefined) return undefined; + + const { name, email } = commiter; + + return { + "@type": "Person", + name, + email, + }; + }), +}; + +const { Content } = await render(post); + +post = undefined; +--- + +<Base + title={linkedData.headline} + description={linkedData.abstract ?? linkedData.headline} +> + <main + itemprop="mainContentOfPage" + itemscope + itemtype="https://schema.org/WebPageElement" + > + <article + itemscope + itemtype="http://schema.org/BlogPosting" + itemid={Astro.url.href} + > + <Translations + id={linkedData["@id"]} + lang={linkedData.inLanguage} + workTranslations={linkedData.workTranslations ?? []} + translationOfWork={linkedData.translationOfWork} + /> + <hgroup> + <h1 itemprop="headline">{linkedData.headline}</h1> + { + linkedData.alternativeHeadline && ( + <p itemprop="alternativeHeadline" class="subtitle"> + {linkedData.alternativeHeadline} + </p> + ) + } + </hgroup> + { + linkedData.abstract && + ( + <section itemprop="abstract"> + <h2>Resumo</h2> + { + linkedData.abstract.split(new RegExp("\\s{2,}")) + .map(( + x, + ) => <p>{x}</p>) + } + </section> + ) + } + {verification && <Signature {lang} {verification} />} + <footer> + { + verification?.verifications && + ( + <Authors + verifications={verification.verifications} + expectedSigners={signers} + commitSignerKey={commit?.signature?.key.long} + /> + ) + } + <dl> + <dt>Data de criação</dt> + <dd> + <DateTime + date={new Date(linkedData.dateCreated)} + locales={linkedData.inLanguage} + options={{ + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZoneName: "long", + }} + itemprop="dateCreated" + /> + </dd> + { + linkedData.dateModified && ( + <dt>Última atualização</dt> + <dd> + <DateTime + date={new Date(linkedData.dateModified)} + locales={linkedData.inLanguage} + options={{ + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZoneName: "long", + }} + itemprop="dateModified" + /> + </dd> + ) + } + { + linkedData.locationCreated && ( + <div + itemprop="locationCreated" + itemscope + itemtype="https://schema.org/Place" + > + <dt>Local de criação</dt> + <dd itemprop="name">{linkedData.locationCreated.name}</dd> + </div> + ) + } + { + linkedData.wordCount && linkedData.timeRequired && + ( + <> + <dt>Tempo de leitura estimado</dt> + <dd> + <data + itemprop="timeRequired" + value={linkedData.timeRequired} + >~ {estimative}</data> + <data itemprop="wordCount" value={linkedData.wordCount} + >(<bdi>palavras</bdi>: {linkedData.wordCount})</data> + </dd> + </> + ) + } + </dl> + </footer> + <div itemprop="articleBody text"><Content /></div> + { + linkedData.keywords !== undefined && + linkedData.keywords.length > 0 && ( + <div id="keywords"> + <KeywordsList keywords={linkedData.keywords} /> + </div> + ) + } + { + linkedData.citation !== undefined && ( + <Citations citations={linkedData.citation} /> + ) + } + <CopyrightNotice + title={linkedData.headline} + holders={linkedData.copyrightHolder ?? [{ "@type": "Person" }]} + years={new Array( // TODO: get years where there were commits + (dateUpdated?.getFullYear() ?? dateCreated.getFullYear()) - + dateCreated.getFullYear() + 1, + ).fill(dateCreated.getFullYear()).map((x, i) => x + i)} + {license} + /> + </article> + </main> +</Base> + +<script + type="application/ld+json" + is:inline + set:html={JSON.stringify(linkedData)} +/> + +<script type="module" is:inline> + hashchange(); + + window.addEventListener("hashchange", hashchange); + + document.addEventListener( + "click", + function (event) { + if ( + event.target && + event.target instanceof HTMLAnchorElement && + event.target.href === location.href && + location.hash.length > 1 + ) { + requestIdleCallback(function () { + if (!event.defaultPrevented) { + hashchange(); + } + }); + } + }, + false, + ); + + function hashchange() { + let hash; + + try { + hash = decodeURIComponent(location.hash.slice(1)).toLowerCase(); + } catch (e) { + return; + } + + const name = "user-content-" + hash; + const target = document.getElementById(name) || + document.getElementsByName(name)[0]; + + if (target) { + requestIdleCallback(function () { + target.scrollIntoView(); + }); + } + } +</script> + +<style is:inline> + section[data-footnotes].footnotes { + word-wrap: break-word; + } +</style> + +<style> + hgroup { + text-align: center; + } + + .subtitle { + font-weight: lighter; + } + + #keywords { + display: flex; + margin-inline: auto; + margin-block: calc(var(--size-4) * 1em); + } + + [itemprop~="articleBody"] { + line-height: 1.4; + font-size: 1.2em; + text-align: justify; + + & h1, + & h2, + & h3 { + line-height: 1.2; + } + + border-block: 1px solid var(--color-dark); + padding-block: calc(var(--size-4) * 1em); + } + + [itemprop="abstract"] { + margin-inline: 1em; + padding-block: 1em; + font-style: italic; + } + + @media print { + body { + font-size: 1rem; + font-family: var(--ff-serif); + line-height: 1.62; + } + } +</style> |