Improve HTML rendering in Markdown and update layouts

This commit is contained in:
Bad Manners 2024-08-26 12:54:24 -03:00
parent 4194154818
commit 21a77ed254
40 changed files with 282 additions and 176 deletions

4
package-lock.json generated
View file

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

View file

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

View file

@ -1,10 +1,10 @@
---
import AgeRestrictedScriptInline from "./AgeRestrictedScriptInline.astro";
import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
import { IconTriangleExclamation } from "./icons";
---
<div
style={{display: "none"}}
style={{ display: "none" }}
id="modal-age-restricted"
class="fixed inset-0 bg-stone-100 dark:bg-stone-900"
role="dialog"
@ -14,7 +14,10 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
<div class="text-bm-500 dark:text-bm-400">
<IconTriangleExclamation width="3rem" height="3rem" />
</div>
<div id="title-age-restricted" class="pb-3 pt-2 text-3xl font-light text-stone-700 sm:pb-4 sm:pt-2 dark:text-stone-50">
<div
id="title-age-restricted"
class="pb-3 pt-2 text-3xl font-light text-stone-700 sm:pb-4 sm:pt-2 dark:text-stone-50"
>
Age verification
</div>
<div
@ -28,7 +31,7 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
</p>
<div class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row">
<button
style={{display: "none"}}
style={{ display: "none" }}
data-modal-reject
id="age-verification-reject"
class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-stone-300 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"
@ -36,7 +39,7 @@ import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
Cancel
</button>
<button
style={{display: "none"}}
style={{ display: "none" }}
data-modal-accept
id="age-verification-accept"
class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"

View file

@ -1,7 +1,6 @@
---
import type { Lang } from "../i18n";
import IconFavorites from "./icons/IconFavorites.astro";
import IconReblogs from "./icons/IconReblogs.astro";
import { IconStar, IconRetweet } from "./icons";
type Props = {
lang: Lang;
@ -112,11 +111,11 @@ const { link, instance, user, postId } = Astro.props;
<div class="ml-1 flex flex-row pb-2 pt-1">
<div class="flex" aria-label="Favorites">
<span data-favorites></span>
<IconFavorites width="1.25rem" height="1.25rem" class="ml-2" />
<IconStar width="1.25rem" height="1.25rem" class="ml-2" />
</div>
<div class="ml-4 flex" aria-label="Reblogs">
<span data-reblogs></span>
<IconReblogs width="1.25rem" height="1.25rem" class="ml-2" />
<IconRetweet width="1.25rem" height="1.25rem" class="ml-2" />
</div>
</div>
<div data-comment-thread class="-mb-2" aria-hidden="true" aria-label="Replies"></div>

View file

@ -18,7 +18,12 @@ const link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!
{
link ? (
<a rel={clsx(rel, "nofollow")} href={link} class:list={[className, "h-card u-url p-name text-link underline"]} target="_blank">
<a
rel={clsx(rel, "nofollow")}
href={link}
class:list={[className, "h-card u-url p-name text-link underline"]}
target="_blank"
>
{username}
</a>
) : (

View file

@ -9,5 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 448 512">
<path d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" />
<path
d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
></path>
</SVGIcon>

View file

@ -9,5 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 320 512">
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
<path
d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"
></path>
</SVGIcon>

View file

@ -9,5 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 320 512">
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
<path
d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"
></path>
</SVGIcon>

View file

@ -0,0 +1,15 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
></path>
</SVGIcon>

View file

@ -1,15 +0,0 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 576 512">
<path
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
></path>
</SVGIcon>

View file

@ -9,5 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 640 512">
<path d="M192 64C86 64 0 150 0 256S86 448 192 448l256 0c106 0 192-86 192-192s-86-192-192-192L192 64zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24l0 32 32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0 0 32c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-32-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l32 0 0-32z" />
<path
d="M192 64C86 64 0 150 0 256S86 448 192 448l256 0c106 0 192-86 192-192s-86-192-192-192L192 64zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24l0 32 32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0 0 32c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-32-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l32 0 0-32z"
></path>
</SVGIcon>

View file

@ -1,15 +0,0 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
></path>
</SVGIcon>

View file

@ -9,5 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" />
<path
d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"
></path>
</SVGIcon>

View file

@ -1,15 +0,0 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path
d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
></path>
</SVGIcon>

View file

@ -0,0 +1,15 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 576 512">
<path
d="M272 416c17.7 0 32-14.3 32-32s-14.3-32-32-32l-112 0c-17.7 0-32-14.3-32-32l0-128 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 128c0 53 43 96 96 96l112 0zM304 96c-17.7 0-32 14.3-32 32s14.3 32 32 32l112 0c17.7 0 32 14.3 32 32l0 128-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-128c0-53-43-96-96-96L304 96z"
></path>
</SVGIcon>

View file

@ -0,0 +1,15 @@
---
import SVGIcon from "./SVGIcon.astro";
type Props = {
width: string;
height: string;
class?: string;
};
---
<SVGIcon {...Astro.props} viewBox="0 0 576 512">
<path
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
></path>
</SVGIcon>

View file

@ -9,6 +9,7 @@ type Props = {
---
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
<path d="M345 39.1L472.8 168.4c52.4 53 52.4 138.2 0 191.2L360.8 472.9c-9.3 9.4-24.5 9.5-33.9 .2s-9.5-24.5-.2-33.9L438.6 325.9c33.9-34.3 33.9-89.4 0-123.7L310.9 72.9c-9.3-9.4-9.2-24.6 .2-33.9s24.6-9.2 33.9 .2zM0 229.5L0 80C0 53.5 21.5 32 48 32l149.5 0c17 0 33.3 6.7 45.3 18.7l168 168c25 25 25 65.5 0 90.5L277.3 442.7c-25 25-65.5 25-90.5 0l-168-168C6.7 262.7 0 246.5 0 229.5zM144 144a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
/>
<path
d="M345 39.1L472.8 168.4c52.4 53 52.4 138.2 0 191.2L360.8 472.9c-9.3 9.4-24.5 9.5-33.9 .2s-9.5-24.5-.2-33.9L438.6 325.9c33.9-34.3 33.9-89.4 0-123.7L310.9 72.9c-9.3-9.4-9.2-24.6 .2-33.9s24.6-9.2 33.9 .2zM0 229.5L0 80C0 53.5 21.5 32 48 32l149.5 0c17 0 33.3 6.7 45.3 18.7l168 168c25 25 25 65.5 0 90.5L277.3 442.7c-25 25-65.5 25-90.5 0l-168-168C6.7 262.7 0 246.5 0 229.5zM144 144a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
></path>
</SVGIcon>

View file

@ -0,0 +1,18 @@
export { default as IconArrowBack } from "./IconArrowBack.astro";
export { default as IconArrowUp } from "./IconArrowUp.astro";
export { default as IconBook } from "./IconBook.astro";
export { default as IconBriefcase } from "./IconBriefcase.astro";
export { default as IconChevronLeft } from "./IconChevronLeft.astro";
export { default as IconChevronRight } from "./IconChevronRight.astro";
export { default as IconCircleInfo } from "./IconCircleInfo.astro";
export { default as IconGamepad } from "./IconGamepad.astro";
export { default as IconHome } from "./IconHome.astro";
export { default as IconMagnifyingGlass } from "./IconMagnifyingGlass.astro";
export { default as IconMoon } from "./IconMoon.astro";
export { default as IconRetweet } from "./IconRetweet.astro";
export { default as IconSquareRSS } from "./IconSquareRSS.astro";
export { default as IconStar } from "./IconStar.astro";
export { default as IconSun } from "./IconSun.astro";
export { default as IconTags } from "./IconTags.astro";
export { default as IconTriangleExclamation } from "./IconTriangleExclamation.astro";
export { default as SVGIcon } from "./SVGIcon.astro";

View file

@ -3,15 +3,17 @@ import { getImage } from "astro:assets";
import BaseLayout from "./BaseLayout.astro";
import logoBM from "../assets/images/logo_bm.png";
import { t } from "../i18n";
import IconHome from "../components/icons/IconHome.astro";
import IconBriefcase from "../components/icons/IconBriefcase.astro";
import IconSquareRSS from "../components/icons/IconSquareRSS.astro";
import IconSun from "../components/icons/IconSun.astro";
import IconMoon from "../components/icons/IconMoon.astro";
import IconMagnifyingGlass from "../components/icons/IconMagnifyingGlass.astro";
import IconTags from "../components/icons/IconTags.astro";
import IconGamepad from "../components/icons/IconGamepad.astro";
import IconBook from "../components/icons/IconBook.astro";
import {
IconHome,
IconBriefcase,
IconSquareRSS,
IconSun,
IconMoon,
IconMagnifyingGlass,
IconTags,
IconGamepad,
IconBook,
} from "../components/icons";
type Props = {
pageTitle: string;
@ -29,107 +31,85 @@ const isCurrentRoute = (path: string) =>
<BaseLayout pageTitle={pageTitle}>
<Fragment slot="head">
<meta property="og:title" content={pageTitle || "Bad Manners"} />
<slot name="head-description" />
<meta property="og:url" content={Astro.url} />
<meta property="og:image" content={logo.src} />
<meta property="og:image:alt" content="Logo for Bad Manners" />
<slot name="head" />
</Fragment>
<div
class="flex min-h-screen flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
>
<nav
class="h-card static mb-4 flex flex-col items-center bg-bm-300 pt-10 pb-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
class="h-card static mb-4 flex flex-col items-center bg-bm-300 pb-10 pt-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
>
<img
loading="eager"
src={logo.src}
alt="Logo for Bad Manners"
alt="A pixelated metal briefcase over a background with a green gradient."
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 my-2 text-2xl font-semibold">Bad Manners</span>
<ul class="flex flex-col gap-y-2">
<li>
<a
class="u-url group text-link"
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-middle" />
<span class="group-focus:underline group-hover:underline">Main website</span>
<span class="group-hover:underline group-focus:underline">Main website</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
href="/"
aria-current={isCurrentRoute("/") ? "page" : undefined}
>
<a class="u-url text-link group" href="/" aria-current={isCurrentRoute("/") ? "page" : undefined}>
<IconBriefcase width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">Gallery</span>
<span class="group-hover:underline group-focus:underline">Gallery</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
class="u-url text-link group"
href="/stories/1"
aria-current={isCurrentRoute("/stories/1") ? "page" : undefined}
>
<IconBook width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">Stories</span>
<span class="group-hover:underline group-focus:underline">Stories</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
href="/games"
aria-current={isCurrentRoute("/games") ? "page" : undefined}
>
<a class="u-url text-link group" href="/games" aria-current={isCurrentRoute("/games") ? "page" : undefined}>
<IconGamepad width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">Games</span>
<span class="group-hover:underline group-focus:underline">Games</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
href="/tags"
aria-current={isCurrentRoute("/tags") ? "page" : undefined}
>
<a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}>
<IconTags width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">Tags</span>
<span class="group-hover:underline group-focus:underline">Tags</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
class="u-url text-link group"
href="/search"
rel="search"
aria-current={isCurrentRoute("/search") ? "page" : undefined}
>
<IconMagnifyingGlass width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">Search</span>
<span class="group-hover:underline group-focus:underline">Search</span>
</a>
</li>
<li>
<a
class="u-url group text-link"
href="/feed.xml"
>
<a class="u-url text-link group" href="/feed.xml">
<IconSquareRSS width="1.25rem" height="1.25rem" class="inline align-middle" />
<span class="group-focus:underline group-hover:underline">RSS feed</span>
<span class="group-hover:underline group-focus:underline">RSS feed</span>
</a>
</li>
<li>
<button
data-dark-mode
style={{ display: "none" }}
class="group text-link"
>
<IconSun width="1.25rem" height="1.25rem" class="align-middle hidden dark:inline" />
<button data-dark-mode style={{ display: "none" }} class="text-link group">
<IconSun width="1.25rem" height="1.25rem" class="hidden align-middle dark:inline" />
<IconMoon width="1.25rem" height="1.25rem" class="inline align-middle dark:hidden" />
<span class="group-focus:underline group-hover:underline">{t("en", "published_content/toggle_dark_mode")}</span>
</a>
<span class="group-hover:underline group-focus:underline"
>{t("en", "published_content/toggle_dark_mode")}</span
>
</button>
</li>
</ul>
<div class="pt-4 text-center text-xs text-black dark:text-white">
@ -144,7 +124,7 @@ const isCurrentRoute = (path: string) =>
)
} |
</span>
<a class="text-link hover:underline focus:underline" href="/licenses.txt" rel="license">Licenses</a>
<a class="text-link hover:underline focus:underline" href="/licenses.toml" rel="license">Licenses</a>
</div>
</nav>
<main

View file

@ -48,13 +48,17 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
labelArticleSection={t(props.lang, "game/article_aria_label")}
>
<meta
slot="head-description"
slot="head"
property="og:description"
content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)}
/>
<Fragment slot="section-information">
<Authors lang={props.lang}>
{authorsList.map((author) => <UserComponent rel="author nofollow" class="p-author" user={author} lang={props.lang} />)}
{
authorsList.map((author) => (
<UserComponent rel="author nofollow" class="p-author" user={author} lang={props.lang} />
))
}
</Authors>
<div id="platforms">
<p>{t(props.lang, "game/platforms", props.platforms)}</p>

View file

@ -11,13 +11,15 @@ import Prose from "../components/Prose.astro";
import MastodonComments from "../components/MastodonComments.astro";
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
import IconSun from "../components/icons/IconSun.astro";
import IconMoon from "../components/icons/IconMoon.astro";
import IconInformation from "../components/icons/IconInformation.astro";
import IconArrowBack from "../components/icons/IconArrowBack.astro";
import IconChevronLeft from "../components/icons/IconChevronLeft.astro";
import IconChevronRight from "../components/icons/IconChevronRight.astro";
import IconArrowUp from "../components/icons/IconArrowUp.astro";
import {
IconSun,
IconMoon,
IconCircleInfo,
IconArrowBack,
IconChevronLeft,
IconChevronRight,
IconArrowUp,
} from "../components/icons";
interface RelatedContent {
link: string;
@ -91,8 +93,10 @@ const thumbnail =
<BaseLayout pageTitle={props.title} lang={props.lang}>
<Fragment slot="head">
{ props.isDraft ? (
<meta name="robots" content="noindex" />
) : null }
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
<slot name="head-description" />
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
{
thumbnail ? (
@ -106,6 +110,7 @@ const thumbnail =
</Fragment>
) : null
}
<slot name="head" />
</Fragment>
<div
id="top"
@ -136,7 +141,7 @@ const thumbnail =
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
aria-labelledby="label-go-to-description"
>
<IconInformation width="1.25rem" height="1.25rem" />
<IconCircleInfo width="1.25rem" height="1.25rem" />
<span class="sr-only" id="label-go-to-description"
>{t(props.lang, "published_content/go_to_description")}</span
>
@ -395,7 +400,7 @@ const thumbnail =
<span
set:html={t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())}
/><span>&nbsp;|</span>
<a class="hover:underline focus:underline" href="/licenses.txt" rel="license"
<a class="hover:underline focus:underline" href="/licenses.toml" rel="license"
>{t(props.lang, "published_content/licenses")}</a
>
</div>

View file

@ -59,7 +59,7 @@ const wordCount = props.wordCount?.toString();
labelArticleSection={t(props.lang, "story/article_aria_label")}
>
<meta
slot="head-description"
slot="head"
property="og:description"
content={t(props.lang, "story/warnings", wordCount, props.contentWarning)}
/>

View file

@ -3,7 +3,7 @@ import GalleryLayout from "../layouts/GalleryLayout.astro";
---
<GalleryLayout pageTitle="404">
<meta slot="head-description" property="og:description" content="Not found" />
<meta slot="head" property="og:description" content="Not found" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">404 &ndash; Not Found</h1>
<p class="my-4">The requested link couldn't be found. Make sure that the URL is correct.</p>
</GalleryLayout>

View file

@ -8,6 +8,7 @@ import { getUsernameForLang } from "../../../utils/get_username_for_lang";
import { isAnonymousUser } from "../../../utils/is_anonymous_user";
import { qualifyLocalURLsInMarkdown } from "../../../utils/qualify_local_urls_in_markdown";
import { getWebsiteLinkForUser } from "../../../utils/get_website_link_for_user";
import { toPlainMarkdown } from "../../../utils/to_plain_markdown";
interface ExportWebsiteInfo {
website: PostWebsite;
@ -86,13 +87,19 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
}
const acc = await promise;
const newData = await qualifyLocalURLsInMarkdown(data, lang, site, exportWebsite);
return acc ? `${acc}\n\n${newData}` : newData;
return `${acc}\n\n${newData}`;
}, Promise.resolve(""));
switch (exportFormat) {
case "bbcode":
return { descriptionFilename: `description_${exportWebsite}.txt`, descriptionText: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n") };
return {
descriptionFilename: `description_${exportWebsite}.txt`,
descriptionText: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n"),
};
case "markdown":
return { descriptionFilename: `description_${exportWebsite}.md`, descriptionText: storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim() };
return {
descriptionFilename: `description_${exportWebsite}.md`,
descriptionText: toPlainMarkdown(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim(),
};
default:
const unknown: never = exportFormat;
throw new Error(`Unknown export format "${unknown}"`);

View file

@ -18,7 +18,7 @@ const games = await Promise.all(
---
<GalleryLayout pageTitle="Games" class="h-feed">
<meta slot="head-description" property="og:description" content="Bad Manners || A game that I've gone and done." />
<meta slot="head" property="og:description" content="Bad Manners || A game that I've gone and done." />
<h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Games</h1>
<p class="p-summary my-4">A game that I've gone and done.</p>
<ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">

View file

@ -74,7 +74,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
---
<GalleryLayout pageTitle="Gallery" class="h-feed">
<meta slot="head-description" property="og:description" content="Bad Manners || Welcome to my gallery!" />
<meta slot="head" property="og:description" content="Bad Manners || Welcome to my gallery!" />
<h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
<div class="p-summary">
<p class="my-4">
@ -91,8 +91,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
class="text-link underline"
href="https://badmanners.xyz/"
data-age-restricted
rel="me"
>my main website</a
rel="me">my main website</a
>.
</p>
</div>

View file

@ -4,7 +4,7 @@ import GalleryLayout from "../layouts/GalleryLayout.astro";
---
<GalleryLayout pageTitle="Search">
<meta slot="head-description" property="og:description" content="Bad Manners || Search" />
<meta slot="head" property="og:description" content="Bad Manners || Search" />
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Search</h1>
<SearchComponent id="search" className="pagefind-ui my-4" />
</GalleryLayout>

View file

@ -29,7 +29,7 @@ const totalPages = Math.ceil(page.total / page.size);
---
<GalleryLayout pageTitle="Stories" class="h-feed">
<meta slot="head-description" property="og:description" content={`Bad Manners || ${page.total} stories.`} />
<meta slot="head" property="og:description" content={`Bad Manners || ${page.total} stories.`} />
<h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1>
<div class="p-summary">
<p class="my-4">The bulk of my content!</p>
@ -44,7 +44,11 @@ const totalPages = Math.ceil(page.total / page.size);
<div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
{
page.url.prev && (
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" rel="prev" href={page.url.prev}>
<a
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
rel="prev"
href={page.url.prev}
>
Previous page
</a>
)
@ -129,7 +133,11 @@ const totalPages = Math.ceil(page.total / page.size);
<div class="mx-auto my-6 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
{
page.url.prev && (
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" rel="prev" href={page.url.prev}>
<a
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
rel="prev"
href={page.url.prev}
>
Previous page
</a>
)

View file

@ -22,7 +22,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
<GalleryLayout pageTitle={series.data.name} enablePagefind={true} class="h-feed">
<meta
slot="head-description"
slot="head"
property="og:description"
content="The Lost of the Marshes || The story of Quince, Nikili, and Suu."
/>
@ -123,7 +123,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
<Image
class="mx-auto w-full max-w-4xl break-before-page"
src={mapImage}
alt="A geopolitical map for the setting of The Lost of the Marshes"
alt="A geopolitical map for the setting of The Lost of the Marshes. The center is covered by desert plains, except for two rivers that run from north to south through the marshes: the First River to the west, and the Second River to the East. The First River starts at the Eastern Toroza Mountains, passing through the Labla state before reaching the Quuwa Marshes inside of Kaati. Along the way, it passes next to the cities of Kaati to its east and Fereh to its west, respectively. The Second River passes by Sinipin to its west. South of the Quuwa marshes and between both rivers are Logas to the east and Kuir to the west, until both rivers reach the Hamora Lake inside of the Hamora Marshes. To the north of the lake is Zugul, and out of the lake comes the southeastern-bound Last River. Halfway down the Last River is Kyorna to the northwest, before it eventually reaches the Bronze Gulf. To the east of the Kaati state is Zuit on the Szogors Mountains, including Saisa; to the south is the state of Munigad; and to the west, an empty desert called Fool's Hope."
data-pagefind-meta="image[src],image_alt[alt]"
/>
</section>

View file

@ -72,7 +72,7 @@ if (uncategorizedTagsSet.size > 0) {
<GalleryLayout pageTitle="Tags">
<meta
property="og:description"
slot="head-description"
slot="head"
content="Bad Manners || Find all content with a specific tag."
/>
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">All available tags</h1>

View file

@ -22,7 +22,10 @@ const { badTag } = Astro.props;
---
<GalleryLayout pageTitle={`Works tagged "${badTag}"`}>
<meta slot="head-description" content="No." property="og:description" />
<Fragment slot="head">
<meta content="No." property="og:description" />
<meta name="robots" content="noindex" />
</Fragment>
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{badTag}"</h1>
<p class="my-4">No.</p>
</GalleryLayout>

View file

@ -121,7 +121,7 @@ const totalWorksWithTag = t(
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
<meta
slot="head-description"
slot="head"
content={`Bad Manners || ${totalWorksWithTag || props.tag}`}
property="og:description"
/>

View file

@ -2,10 +2,10 @@ import type { CollectionEntry } from "astro:content";
import { DEFAULT_LANG, type UserWebsite } from "../content/config";
import { getUsernameForLang } from "./get_username_for_lang";
type WebsiteUserData<W extends UserWebsite> = CollectionEntry<"users">["data"]["links"][W];
type WebsiteUserData<W extends UserWebsite> = NonNullable<CollectionEntry<"users">["data"]["links"][W]>;
function getUserDataForWebsite<W extends UserWebsite>(user: CollectionEntry<"users">, website: W) {
const link: WebsiteUserData<W> | undefined = user.data.links[website];
function getUserDataForWebsite<W extends UserWebsite>(user: CollectionEntry<"users">, website: W): WebsiteUserData<W> {
const link = user.data.links[website];
if (link) {
return link;
}

View file

@ -1,5 +1,6 @@
import { Marked, type RendererObject } from "marked";
import { decode } from "tiny-decode";
import { parsePartialHTMLTag } from "./parse_partial_html_tag";
const renderer: RendererObject = {
code({ text }) {
@ -11,15 +12,21 @@ const renderer: RendererObject = {
html(token) {
const { type, raw } = token;
if (type === "html") {
if (raw === "</a>") {
const tag = parsePartialHTMLTag(raw);
switch (tag.tag) {
case "a":
if (tag.type === "open") {
if (tag.attributes?.href) {
return `[url=${tag.attributes.href}]`;
}
} else if (tag.type === "close") {
return "[/url]";
}
const match = raw.match(/^<a(?:\w+[a-z-]+(?:="[^"]*"))*\w+href="([^"]+)"(?:\w+[a-z-]+(?:="[^"]*"))*\w*>$/);
if (match&&match[1]) {
return `[url=${match[1]}]`;
console.error("ERROR: unhandled anchor in markdownToBbcode.html", token, tag);
throw new Error("Unhandled anchor HTML token in BBCode renderer");
}
}
console.error("ERROR: unhandled token markdownToBbcode.html", token);
console.error("ERROR: unhandled token in markdownToBbcode.html", token);
throw new Error("Unknown HTML token not supported by BBCode renderer");
},
heading({ tokens }) {
@ -36,7 +43,7 @@ const renderer: RendererObject = {
return `[li]${this.parser.parseInline(tokens)}[/li]\n`;
},
checkbox(token) {
console.error("ERROR: unhandled token markdownToBbcode.checkbox", token);
console.error("ERROR: unhandled token in markdownToBbcode.checkbox", token);
throw new Error("Not supported by BBCode: checkbox");
},
paragraph({ tokens }) {
@ -73,7 +80,7 @@ const renderer: RendererObject = {
return `[url=${href}]${this.parser.parseInline(tokens)}[/url]`;
},
image({ href }) {
return `[img]${href}[/url]`;
return `[img]${href}[/img]`;
},
};

View file

@ -24,7 +24,7 @@ const renderer: RendererObject = {
return `- ${this.parser.parseInline(tokens)}\n`;
},
checkbox(token) {
console.error("ERROR: unhandled token markdownToPlaintext.checkbox", token);
console.error("ERROR: unhandled token in markdownToPlaintext.checkbox", token);
throw new Error("Not supported by plaintext renderer: checkbox");
},
paragraph({ tokens }) {
@ -54,15 +54,14 @@ const renderer: RendererObject = {
return "\n\n";
},
del(token) {
console.error("ERROR: unhandled token markdownToPlaintext.del", token);
console.error("ERROR: unhandled token in markdownToPlaintext.del", token);
throw new Error("Not supported by plaintext renderer: del");
},
link({ tokens }) {
return this.parser.parseInline(tokens);
},
image(token) {
console.error("ERROR: unhandled token markdownToPlaintext.image", token);
throw new Error("Not supported by plaintext renderer: img");
image() {
return "";
},
};

View file

@ -0,0 +1,53 @@
interface ParsedHTMLTag {
tag: string;
type: "open" | "close" | "both";
attributes?: Record<string, string|null>;
}
const OPEN_TAG_START_REGEX = /^<\s*([a-z-]+)\s*/;
const ATTRIBUTE_REGEX = /^([a-z-]+)(?:=("[^"]*"))?\s*/;
const END_OPEN_REGEX = /^>$/;
const END_OPEN_CLOSE_REGEX = /^\/>$/;
const CLOSE_TAG_REGEX = /^<\/\s*([a-z-]+)\s*>$/;
/**
* Parses the opening or closing half of an HTML tag. To parse well-formed HTML, use DOMParser.parseFromString().
*/
export function parsePartialHTMLTag(text: string): ParsedHTMLTag {
let partialText = text;
const openTag = partialText.match(OPEN_TAG_START_REGEX);
if (openTag) {
const result: ParsedHTMLTag = {
tag: openTag[1],
type: "open",
};
partialText = partialText.slice(openTag[0].length);
while (true) {
const attribute = partialText.match(ATTRIBUTE_REGEX);
if (!attribute) {
break;
}
if (!result.attributes) {
result.attributes = {};
}
result.attributes[attribute[1]] = attribute[2] ? JSON.parse(attribute[2]) : null;
partialText = partialText.slice(attribute[0].length);
}
if (partialText.match(END_OPEN_REGEX)) {
return result;
}
if (partialText.match(END_OPEN_CLOSE_REGEX)) {
result.type = "both";
return result;
}
} else {
const closeTag = partialText.match(CLOSE_TAG_REGEX);
if (closeTag) {
return {
tag: closeTag[1],
type: "close",
}
}
}
throw new Error(`Unable to parse partial HTML tag: ${text}`);
}

View file

@ -0,0 +1,5 @@
/**
* Replaces HTML in Markdown text with its simplified representation.
*/
export const toPlainMarkdown = (text: string) =>
text.replaceAll(/<\s*a(?:\s+\S+)*(?:\s+href="([^"]+)")(?:\s+\S+)*\s*>\s*([^<]+)\s*<\/a>/g, "[$2]($1)");