Use history.replaceState ageVerified query and improve export-story script
This commit is contained in:
parent
21a77ed254
commit
fb30f1b416
16 changed files with 96 additions and 83 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.7",
|
"version": "1.7.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.7",
|
"version": "1.7.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.7.7",
|
"version": "1.7.8",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "astro sync",
|
"postinstall": "astro sync",
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { spawn, spawnSync } from "node:child_process";
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
|
import { readdirSync, mkdirSync } from "node:fs";
|
||||||
|
import { mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join, normalize } from "node:path";
|
import { join, normalize } from "node:path";
|
||||||
import { createInterface } from "node:readline";
|
import { createInterface } from "node:readline";
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import fetchRetryWrapper from "fetch-retry";
|
import fetchRetryWrapper from "fetch-retry";
|
||||||
|
import type { HealthcheckResponse } from "../src/pages/api/healthcheck";
|
||||||
|
import type { ExportStoryResponse } from "../src/pages/api/export-story/[...slug]";
|
||||||
|
|
||||||
function getRTFStyles(rtfSource: string) {
|
function getRTFStyles(rtfSource: string) {
|
||||||
const matches = rtfSource.matchAll(
|
const matches = rtfSource.matchAll(
|
||||||
|
@ -53,10 +56,10 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
const outputDir = normalize(options.outputDir);
|
const outputDir = normalize(options.outputDir);
|
||||||
let files: string[];
|
let files: string[];
|
||||||
try {
|
try {
|
||||||
files = await readdir(outputDir);
|
files = readdirSync(outputDir);
|
||||||
} catch {
|
} catch {
|
||||||
files = [];
|
files = [];
|
||||||
console.log(`Created directory at ${await mkdir(outputDir, { recursive: true })}`);
|
console.log(`Created directory at ${mkdirSync(outputDir, { recursive: true })}`);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
console.error(`ERROR: Directory ${outputDir} is not empty!`);
|
console.error(`ERROR: Directory ${outputDir} is not empty!`);
|
||||||
|
@ -84,7 +87,7 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
const healthcheck: { isAlive: boolean } = await response.json();
|
const healthcheck: HealthcheckResponse = await response.json();
|
||||||
if (healthcheck.isAlive !== true) {
|
if (healthcheck.isAlive !== true) {
|
||||||
throw new Error(JSON.stringify(healthcheck));
|
throw new Error(JSON.stringify(healthcheck));
|
||||||
}
|
}
|
||||||
|
@ -102,32 +105,41 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
console.log("Getting data from Astro...");
|
console.log("Getting data from Astro...");
|
||||||
const response = await fetch(new URL(`/api/export-story/${slug}`, astroURL));
|
const response = await fetch(new URL(`/api/export-story/${slug}`, astroURL));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to reach API (status code ${response.status})`);
|
throw new Error(`Failed to reach export-story API (status code ${response.status})`);
|
||||||
}
|
}
|
||||||
const data: { story: string; description: Record<string, string>; thumbnail: string | null } =
|
const data: ExportStoryResponse = await response.json();
|
||||||
await response.json();
|
// Process response fields in parallel
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(data.description).map(async ([filename, description]) => {
|
[
|
||||||
return await writeFile(join(outputDir, filename), description);
|
// Story
|
||||||
}),
|
(async () => {
|
||||||
|
storyText = data.story;
|
||||||
|
await writeFile(join(outputDir, `${slug}.txt`), storyText);
|
||||||
|
})(),
|
||||||
|
// Descriptions
|
||||||
|
Object.entries(data.description).map(
|
||||||
|
async ([filename, description]) => await writeFile(join(outputDir, filename), description),
|
||||||
|
),
|
||||||
|
// Thumbnail
|
||||||
|
(async () => {
|
||||||
|
if (data.thumbnail) {
|
||||||
|
if (data.thumbnail.startsWith("/@fs/")) {
|
||||||
|
const thumbnailPath = data.thumbnail
|
||||||
|
.replace(/^\/@fs/, "")
|
||||||
|
.replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
|
||||||
|
await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
||||||
|
} else {
|
||||||
|
const thumbnail = await fetchRetry(data.thumbnail, { retries: 2, retryDelay: 10000 });
|
||||||
|
if (!thumbnail.ok) {
|
||||||
|
throw new Error("Failed to get thumbnail");
|
||||||
|
}
|
||||||
|
const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg";
|
||||||
|
await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
].flat(),
|
||||||
);
|
);
|
||||||
if (data.thumbnail) {
|
|
||||||
if (data.thumbnail.startsWith("/@fs/")) {
|
|
||||||
const thumbnailPath = data.thumbnail
|
|
||||||
.replace(/^\/@fs/, "")
|
|
||||||
.replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
|
|
||||||
await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
|
||||||
} else {
|
|
||||||
const thumbnail = await fetch(data.thumbnail);
|
|
||||||
if (!thumbnail.ok) {
|
|
||||||
throw new Error("Failed to get thumbnail");
|
|
||||||
}
|
|
||||||
const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg";
|
|
||||||
await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storyText = data.story;
|
|
||||||
writeFile(join(outputDir, `${slug}.txt`), storyText);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (devServerProcess) {
|
if (devServerProcess) {
|
||||||
console.log("Shutting down the Astro development server...");
|
console.log("Shutting down the Astro development server...");
|
||||||
|
@ -139,18 +151,29 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
|
|
||||||
/* Parse story into output formats */
|
/* Parse story into output formats */
|
||||||
console.log("Parsing story into output formats...");
|
console.log("Parsing story into output formats...");
|
||||||
await writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
// Process output files in parallel
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
|
await Promise.all([
|
||||||
await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
// ${slug}.md
|
||||||
spawnSync("libreoffice", ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")], {
|
writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*")),
|
||||||
stdio: "ignore",
|
// ${slug}.rtf
|
||||||
});
|
(async () => {
|
||||||
const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
|
const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
|
||||||
const rtfStyles = getRTFStyles(rtfText);
|
await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||||
await writeFile(
|
spawnSync(
|
||||||
join(outputDir, `${slug}.rtf`),
|
"libreoffice",
|
||||||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")],
|
||||||
);
|
{
|
||||||
|
stdio: "ignore",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
|
||||||
|
const rtfStyles = getRTFStyles(rtfText);
|
||||||
|
await writeFile(
|
||||||
|
join(outputDir, `${slug}.rtf`),
|
||||||
|
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
console.log("Success!");
|
console.log("Success!");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ import { IconTriangleExclamation } from "./icons";
|
||||||
<AgeRestrictedScriptInline />
|
<AgeRestrictedScriptInline />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ENABLE_VIEW_TRANSITIONS = false;
|
|
||||||
type AgeVerified = "true" | undefined;
|
type AgeVerified = "true" | undefined;
|
||||||
|
|
||||||
const ageRestrictedModalSetup = () => {
|
const ageRestrictedModalSetup = () => {
|
||||||
|
@ -97,9 +96,6 @@ import { IconTriangleExclamation } from "./icons";
|
||||||
rejectButton.focus();
|
rejectButton.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (ENABLE_VIEW_TRANSITIONS) {
|
|
||||||
document.addEventListener("astro:page-load", ageRestrictedModalSetup);
|
ageRestrictedModalSetup();
|
||||||
} else {
|
|
||||||
ageRestrictedModalSetup();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
|
|
||||||
<script is:inline>function a(){let b=document,c="#modal-age-restricted",d="true",e=b.querySelector("body > "+c),f="ageVerified",g="searchParams",h=localStorage;new URL(b.location)[g].get(f)===d&&(h[f]=d);e&&(h[f]===d?b.querySelectorAll("a[href][data-age-restricted]").forEach(x=>{let y=new URL(x.href);y[g].set(f,d);x.href=y.href}):((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(x=>x.setAttribute("inert",d)),(e.style.display="block")))};document.addEventListener("astro:after-swap",a);a()</script>
|
<script is:inline>(function (){let b=document,c="#modal-age-restricted",d="true",e=b.querySelector("body > "+c),f="ageVerified",g="searchParams",h=localStorage,i=new URL(b.location),j=history;i[g].get(f)===d&&(h[f]=d,j&&(i[g].delete(f),j.replaceState({},"",i)));e&&(h[f]===d?b.querySelectorAll("a[href][data-age-restricted]").forEach(x=>{let y=new URL(x.href);y[g].set(f,d);x.href=y.href}):((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(x=>x.setAttribute("inert",d)),(e.style.display="block")))})()</script>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro";
|
||||||
<DarkModeScriptInline />
|
<DarkModeScriptInline />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ENABLE_VIEW_TRANSITIONS = false;
|
|
||||||
type ColorScheme = "auto" | "dark" | "light" | undefined;
|
type ColorScheme = "auto" | "dark" | "light" | undefined;
|
||||||
|
|
||||||
const colorSchemeSetup = () => {
|
const colorSchemeSetup = () => {
|
||||||
|
@ -31,9 +30,6 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro";
|
||||||
button.setAttribute("aria-hidden", "false");
|
button.setAttribute("aria-hidden", "false");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (ENABLE_VIEW_TRANSITIONS) {
|
|
||||||
document.addEventListener("astro:page-load", colorSchemeSetup);
|
colorSchemeSetup();
|
||||||
} else {
|
|
||||||
colorSchemeSetup();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
|
|
||||||
<script is:inline>function a(){var b="dark",c="auto",d="colorScheme",e=document.body.classList,f=localStorage,g=f&&f[d];g&&g!==c?g===b&&e.add(b):(f&&(f[d]=c),matchMedia("(prefers-color-scheme: dark)").matches&&e.add(b))};document.addEventListener("astro:after-swap",a);a()</script>
|
<script is:inline>(function (){var b="dark",c="auto",d="colorScheme",e=document.body.classList,f=localStorage,g=f&&f[d];g&&g!==c?g===b&&e.add(b):(f&&(f[d]=c),matchMedia("(prefers-color-scheme: dark)").matches&&e.add(b))})()</script>
|
||||||
|
|
|
@ -123,8 +123,6 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ENABLE_VIEW_TRANSITIONS = false;
|
|
||||||
|
|
||||||
interface MastodonPost {
|
interface MastodonPost {
|
||||||
link: string;
|
link: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
|
@ -322,9 +320,5 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
loadCommentsButton.style.removeProperty("display");
|
loadCommentsButton.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ENABLE_VIEW_TRANSITIONS) {
|
initCommentSection();
|
||||||
document.addEventListener("astro:page-load", initCommentSection);
|
|
||||||
} else {
|
|
||||||
initCommentSection();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -93,9 +93,7 @@ const thumbnail =
|
||||||
|
|
||||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
{ props.isDraft ? (
|
{props.isDraft ? <meta name="robots" content="noindex" /> : null}
|
||||||
<meta name="robots" content="noindex" />
|
|
||||||
) : null }
|
|
||||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,5 +5,5 @@ import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
<GalleryLayout pageTitle="404">
|
<GalleryLayout pageTitle="404">
|
||||||
<meta slot="head" 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 – Not Found</h1>
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">404 – Not Found</h1>
|
||||||
<p class="my-4">The requested link couldn't be found. Make sure that the URL is correct.</p>
|
<p class="my-4">The requested link could not be found. Make sure that the URL is correct.</p>
|
||||||
</GalleryLayout>
|
</GalleryLayout>
|
||||||
|
|
|
@ -25,6 +25,12 @@ const WEBSITE_LIST = [
|
||||||
|
|
||||||
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
||||||
|
|
||||||
|
export type ExportStoryResponse = {
|
||||||
|
story: string;
|
||||||
|
description: Record<string, string>;
|
||||||
|
thumbnail: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
story: CollectionEntry<"stories">;
|
story: CollectionEntry<"stories">;
|
||||||
};
|
};
|
||||||
|
@ -98,7 +104,9 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return {
|
return {
|
||||||
descriptionFilename: `description_${exportWebsite}.md`,
|
descriptionFilename: `description_${exportWebsite}.md`,
|
||||||
descriptionText: toPlainMarkdown(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim(),
|
descriptionText: toPlainMarkdown(storyDescription)
|
||||||
|
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||||
|
.trim(),
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
const unknown: never = exportFormat;
|
const unknown: never = exportFormat;
|
||||||
|
@ -144,7 +152,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
{} as Record<string, string>,
|
{} as Record<string, string>,
|
||||||
),
|
),
|
||||||
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
||||||
}),
|
} satisfies ExportStoryResponse),
|
||||||
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
export type HealthcheckResponse = {
|
||||||
|
isAlive: true;
|
||||||
|
};
|
||||||
|
|
||||||
export const GET: APIRoute = () => {
|
export const GET: APIRoute = () => {
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify({ isAlive: true }), {
|
return new Response(JSON.stringify({ isAlive: !false } satisfies HealthcheckResponse), {
|
||||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,6 +45,7 @@ async function storyFeedItem(
|
||||||
" ",
|
" ",
|
||||||
),
|
),
|
||||||
categories: ["story"],
|
categories: ["story"],
|
||||||
|
commentsUrl: data.posts.mastodon?.link,
|
||||||
content: sanitizeHtml(
|
content: sanitizeHtml(
|
||||||
`<h1>${data.title}</h1>` +
|
`<h1>${data.title}</h1>` +
|
||||||
`<p>${t(
|
`<p>${t(
|
||||||
|
@ -89,6 +90,7 @@ async function gameFeedItem(
|
||||||
" ",
|
" ",
|
||||||
),
|
),
|
||||||
categories: ["game"],
|
categories: ["game"],
|
||||||
|
commentsUrl: data.posts.mastodon?.link,
|
||||||
content: sanitizeHtml(
|
content: sanitizeHtml(
|
||||||
`<h1>${data.title}</h1>` +
|
`<h1>${data.title}</h1>` +
|
||||||
`<p>${t(
|
`<p>${t(
|
||||||
|
|
|
@ -70,11 +70,7 @@ if (uncategorizedTagsSet.size > 0) {
|
||||||
---
|
---
|
||||||
|
|
||||||
<GalleryLayout pageTitle="Tags">
|
<GalleryLayout pageTitle="Tags">
|
||||||
<meta
|
<meta property="og:description" slot="head" content="Bad Manners || Find all content with a specific tag." />
|
||||||
property="og: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>
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">All available tags</h1>
|
||||||
<p class="my-4">You can find all content with a specific tag by selecting it below from the appropriate category.</p>
|
<p class="my-4">You can find all content with a specific tag by selecting it below from the appropriate category.</p>
|
||||||
<section class="my-2" aria-labelledby="category-series">
|
<section class="my-2" aria-labelledby="category-series">
|
||||||
|
|
|
@ -120,11 +120,7 @@ const totalWorksWithTag = t(
|
||||||
---
|
---
|
||||||
|
|
||||||
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
|
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
|
||||||
<meta
|
<meta slot="head" content={`Bad Manners || ${totalWorksWithTag || props.tag}`} property="og:description" />
|
||||||
slot="head"
|
|
||||||
content={`Bad Manners || ${totalWorksWithTag || props.tag}`}
|
|
||||||
property="og:description"
|
|
||||||
/>
|
|
||||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1>
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1>
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<Prose>
|
<Prose>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
interface ParsedHTMLTag {
|
interface ParsedHTMLTag {
|
||||||
tag: string;
|
tag: string;
|
||||||
type: "open" | "close" | "both";
|
type: "open" | "close" | "both";
|
||||||
attributes?: Record<string, string|null>;
|
attributes?: Record<string, string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPEN_TAG_START_REGEX = /^<\s*([a-z-]+)\s*/;
|
const OPEN_TAG_START_REGEX = /^<\s*([a-z-]+)\s*/;
|
||||||
|
@ -46,8 +46,8 @@ export function parsePartialHTMLTag(text: string): ParsedHTMLTag {
|
||||||
return {
|
return {
|
||||||
tag: closeTag[1],
|
tag: closeTag[1],
|
||||||
type: "close",
|
type: "close",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Unable to parse partial HTML tag: ${text}`);
|
throw new Error(`Unable to parse partial HTML tag: ${text}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue