diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c24d4b4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: nextjs + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: nextjs/package-lock.json + - run: npm ci + - run: npm test diff --git a/nextjs/.gitignore b/nextjs/.gitignore new file mode 100644 index 0000000..57ff971 --- /dev/null +++ b/nextjs/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.next/ diff --git a/nextjs/app/[year]/[month]/page.tsx b/nextjs/app/[year]/[month]/page.tsx new file mode 100644 index 0000000..d4d82a6 --- /dev/null +++ b/nextjs/app/[year]/[month]/page.tsx @@ -0,0 +1,38 @@ +import { notFound } from "next/navigation"; +import { loadAllPostsSync } from "@/lib/content"; +import { enrichPost } from "@/lib/posts"; +import { getMonthArchive } from "@/lib/archives"; +import { monthName } from "@/lib/dates"; +import { ArchiveMonthLayout } from "@/components/ArchiveMonthLayout"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ year: string; month: string }>; +}) { + const { year, month } = await params; + const m = parseInt(month, 10); + const label = m >= 1 && m <= 12 ? `${monthName(m)} ${year}` : `${year}/${month}`; + return { title: label }; +} + +export default async function MonthArchivePage({ + params, +}: { + params: Promise<{ year: string; month: string }>; +}) { + const { year: yearStr, month: monthStr } = await params; + const year = parseInt(yearStr, 10); + const month = parseInt(monthStr, 10); + + if (isNaN(year) || isNaN(month) || month < 1 || month > 12) notFound(); + + const allPosts = loadAllPostsSync().map(enrichPost); + const archive = getMonthArchive(allPosts, year, month); + + if (!archive) notFound(); + + return ; +} diff --git a/nextjs/app/[year]/page.tsx b/nextjs/app/[year]/page.tsx new file mode 100644 index 0000000..3fbc43a --- /dev/null +++ b/nextjs/app/[year]/page.tsx @@ -0,0 +1,34 @@ +import { notFound } from "next/navigation"; +import { loadAllPostsSync } from "@/lib/content"; +import { enrichPost } from "@/lib/posts"; +import { getYearArchive } from "@/lib/archives"; +import { ArchiveYearLayout } from "@/components/ArchiveYearLayout"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ year: string }>; +}) { + const { year } = await params; + return { title: year }; +} + +export default async function YearArchivePage({ + params, +}: { + params: Promise<{ year: string }>; +}) { + const { year: yearStr } = await params; + const year = parseInt(yearStr, 10); + + if (isNaN(year) || year < 2000 || year > 2100) notFound(); + + const allPosts = loadAllPostsSync().map(enrichPost); + const archive = getYearArchive(allPosts, year); + + if (!archive) notFound(); + + return ; +} diff --git a/nextjs/app/about/page.tsx b/nextjs/app/about/page.tsx new file mode 100644 index 0000000..50e0d38 --- /dev/null +++ b/nextjs/app/about/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import { loadPage, renderPageContent } from "@/lib/content"; +import { ArticleLayout } from "@/components/ArticleLayout"; + +export const metadata: Metadata = { + title: "About", +}; + +export default async function AboutPage() { + const page = loadPage("about.html"); + if (!page) return

Page not found.

; + + const content = await renderPageContent(page.content, page.format); + return ; +} diff --git a/nextjs/app/blog/[slug]/page.tsx b/nextjs/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..77c172f --- /dev/null +++ b/nextjs/app/blog/[slug]/page.tsx @@ -0,0 +1,50 @@ +import { notFound } from "next/navigation"; +import { loadAllPostsSync, renderPost } from "@/lib/content"; +import { enrichPost } from "@/lib/posts"; +import { BlogPostLayout } from "@/components/BlogPostLayout"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const allPosts = loadAllPostsSync().map(enrichPost); + const post = allPosts.find((p) => p.slug === slug); + if (!post) return {}; + return { + title: post.frontmatter.title, + ...(post.frontmatter.canonical && { + alternates: { canonical: post.frontmatter.canonical }, + }), + }; +} + +export default async function BlogPostPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const allPosts = loadAllPostsSync().map(enrichPost); + const index = allPosts.findIndex((p) => p.slug === slug); + + if (index === -1) notFound(); + + const renderedPost = await renderPost(allPosts[index]); + const enrichedRendered = { ...allPosts[index], content: renderedPost.content }; + + // Posts sorted newest first: previous = older = index+1, next = newer = index-1 + const previousPost = index < allPosts.length - 1 ? allPosts[index + 1] : null; + const nextPost = index > 0 ? allPosts[index - 1] : null; + + return ( + + ); +} diff --git a/nextjs/app/feed.atom/route.ts b/nextjs/app/feed.atom/route.ts new file mode 100644 index 0000000..b51a851 --- /dev/null +++ b/nextjs/app/feed.atom/route.ts @@ -0,0 +1,79 @@ +import { loadAllPostsSync, renderPost } from "@/lib/content"; +import { enrichPost } from "@/lib/posts"; +import { siteConfig } from "@/config/site"; +import { shortlink } from "@/lib/base60"; +import { generateTagId } from "@/lib/tag-id"; +import { formatISO, toXmlSchema } from "@/lib/dates"; + +export const dynamic = "force-dynamic"; + +function escapeXml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export async function GET() { + const allPosts = loadAllPostsSync().map(enrichPost); + const recentPosts = allPosts.slice(0, 20); + + // Render all posts + const renderedPosts = await Promise.all( + recentPosts.map(async (post) => { + const rendered = await renderPost(post); + return { ...post, content: rendered.content }; + }) + ); + + const entries = renderedPosts + .map((post) => { + const id = post.frontmatter.atomid || generateTagId(post.globalDate, post.cleanUrl); + const url = post.frontmatter.canonical || `${siteConfig.url}${post.cleanUrl}`; + const shortUrl = shortlink(post.globalDate, siteConfig.shortdomain); + const isoDate = formatISO(post.globalDate); + + let dateElements: string; + if (post.frontmatter.updated) { + dateElements = ` ${isoDate}\n ${formatISO(post.frontmatter.updated)}`; + } else { + dateElements = ` ${isoDate}`; + } + + const summary = post.excerpt + ? `\n ${escapeXml(post.excerpt)}` + : ""; + + return ` + ${escapeXml(id)} + ${escapeXml(post.frontmatter.title || "")} + + + ${dateElements}${summary} + ${escapeXml(post.content)} + `; + }) + .join("\n"); + + const xml = ` + + ${siteConfig.title} + + + ${toXmlSchema(new Date())} + ${siteConfig.url} + + ${siteConfig.author.name} + ${siteConfig.author.url} + +${entries} +`; + + return new Response(xml, { + headers: { + "Content-Type": "application/atom+xml; charset=utf-8", + }, + }); +} diff --git a/nextjs/app/feeds/page.tsx b/nextjs/app/feeds/page.tsx new file mode 100644 index 0000000..20d402e --- /dev/null +++ b/nextjs/app/feeds/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import { loadPage, renderPageContent } from "@/lib/content"; +import { ArticleLayout } from "@/components/ArticleLayout"; + +export const metadata: Metadata = { + title: "Feeds and Following", +}; + +export default async function FeedsPage() { + const page = loadPage("feeds.md"); + if (!page) return

Page not found.

; + + const content = await renderPageContent(page.content, page.format); + return ; +} diff --git a/nextjs/app/humans.txt/route.ts b/nextjs/app/humans.txt/route.ts new file mode 100644 index 0000000..8f69397 --- /dev/null +++ b/nextjs/app/humans.txt/route.ts @@ -0,0 +1,22 @@ +import { toXmlSchema } from "@/lib/dates"; + +export const dynamic = "force-dynamic"; + +export function GET() { + const content = `/* TEAM */ + Protagonist: Ben Ward + URL: https://benward.uk/about + Twitter: @benward + From: Cambridge, United Kingdom + +/* SITE */ + Last update: ${toXmlSchema(new Date())} + Language: English (British) + Doctype: HTML5 + IDE: Nova, Visual Studio Code, Sublime Text, iA Writer +`; + + return new Response(content, { + headers: { "Content-Type": "text/plain" }, + }); +} diff --git a/nextjs/app/layout.module.css b/nextjs/app/layout.module.css new file mode 100644 index 0000000..f59cd03 --- /dev/null +++ b/nextjs/app/layout.module.css @@ -0,0 +1,56 @@ +/* Root layout styles: header, footer, page canvas, site nav */ + +.header, +.footer, +.main > article, +.main > section { + padding: 32px; +} + +.main > article, +.main > section { + background: var(--BenWard-pageColor); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.2); +} + +.siteTitle { + font-size: 170%; + margin: 0; + color: var(--BenWard-logoColor); + opacity: 0.25; + transition: opacity .5s ease-out; +} + +.siteTitle a:link, +.siteTitle a:visited { + color: inherit; +} + +.siteTitle:hover { + opacity: 0.75; +} + +.footer { + color: var(--BenWard-footerTextColor); + font-size: 80%; + line-height: 21px; +} + +.footer address { + display: inline; + font-style: normal; +} + +.footer a:link, +.footer a:visited, +.footer a:hover, +.footer a:focus { + color: var(--BenWard-footerLinkColor); +} + +.footnote { + margin-top: 100%; + color: rgba(255,255,255,0.2); + text-shadow: rgba(0, 0, 0, 0.7) -1px 0 2px; + text-align: center; +} diff --git a/nextjs/app/layout.tsx b/nextjs/app/layout.tsx new file mode 100644 index 0000000..05e8f74 --- /dev/null +++ b/nextjs/app/layout.tsx @@ -0,0 +1,100 @@ +import type { Metadata } from "next"; +import { siteConfig } from "@/config/site"; +import { romanize } from "@/lib/romans"; +import { Profiles } from "@/components/Profiles"; +import { Identity } from "@/components/Identity"; +import { Scripts } from "@/components/Scripts"; +import "@/lib/global.css"; +import styles from "./layout.module.css"; + +export const metadata: Metadata = { + title: { + default: siteConfig.title, + template: `%s · ${siteConfig.title}`, + }, + alternates: { + types: { + "application/atom+xml": "/feed.atom", + }, + }, + icons: { + shortcut: "/favicon.png", + }, + other: { + "twitter:widgets:link-color": "#a32226", + "twitter:card": "summary", + "twitter:description": + "benward.uk is the personal web site and blog of Ben Ward.", + "twitter:creator:id": siteConfig.author.twitterId, + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const year = new Date().getFullYear(); + + return ( + + + + + + + + +
+

+ + {siteConfig.title} + +

+
+
{children}
+ + + + + ); +} diff --git a/nextjs/app/network/page.tsx b/nextjs/app/network/page.tsx new file mode 100644 index 0000000..d5baacb --- /dev/null +++ b/nextjs/app/network/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import { loadPage, renderPageContent } from "@/lib/content"; +import { ArticleLayout } from "@/components/ArticleLayout"; + +export const metadata: Metadata = { + title: "Everywhere Else", +}; + +export default async function NetworkPage() { + const page = loadPage("network.md"); + if (!page) return

Page not found.

; + + const content = await renderPageContent(page.content, page.format); + return ; +} diff --git a/nextjs/app/not-found.tsx b/nextjs/app/not-found.tsx new file mode 100644 index 0000000..9830953 --- /dev/null +++ b/nextjs/app/not-found.tsx @@ -0,0 +1,11 @@ +export default function NotFound() { + return ( + + ); +} diff --git a/nextjs/app/page.tsx b/nextjs/app/page.tsx new file mode 100644 index 0000000..87daf46 --- /dev/null +++ b/nextjs/app/page.tsx @@ -0,0 +1,36 @@ +import { loadAllPostsSync } from "@/lib/content"; +import { enrichPost } from "@/lib/posts"; +import { formatArchivePath } from "@/lib/dates"; +import { Cover } from "@/components/Cover"; +import { PostSummary } from "@/components/PostSummary"; + +export const dynamic = "force-dynamic"; + +export default function HomePage() { + const allPosts = loadAllPostsSync().map(enrichPost); + const recentPosts = allPosts.slice(0, 10); + const lastPost = recentPosts[recentPosts.length - 1]; + const lastDate = lastPost ? formatArchivePath(lastPost.globalDate) : ""; + + return ( +
+ +

Recent Posts

+
    + {recentPosts.map((post) => ( +
  1. + +
  2. + ))} +
+ +
+ ); +} diff --git a/nextjs/app/robots.txt/route.ts b/nextjs/app/robots.txt/route.ts new file mode 100644 index 0000000..88d72c9 --- /dev/null +++ b/nextjs/app/robots.txt/route.ts @@ -0,0 +1,15 @@ +export function GET() { + const content = `User-Agent: * +# Don't index tag index pages. Duplicates date archives/articles +Disallow: /blog/tags/* +Disallow: /blog/categories/* +Disallow: /tags/* +Disallow: /res +Disallow: /media +Disallow: /files +`; + + return new Response(content, { + headers: { "Content-Type": "text/plain" }, + }); +} diff --git a/nextjs/app/s/[id]/route.ts b/nextjs/app/s/[id]/route.ts new file mode 100644 index 0000000..e57a236 --- /dev/null +++ b/nextjs/app/s/[id]/route.ts @@ -0,0 +1,18 @@ +import { redirect } from "next/navigation"; +import { resolveShortlink } from "@/lib/shortlinks"; + +export const dynamic = "force-dynamic"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const url = resolveShortlink(id); + + if (!url) { + return new Response("Shortlink not found", { status: 404 }); + } + + redirect(url); +} diff --git a/nextjs/app/sitemap.xml/route.ts b/nextjs/app/sitemap.xml/route.ts new file mode 100644 index 0000000..eae7c33 --- /dev/null +++ b/nextjs/app/sitemap.xml/route.ts @@ -0,0 +1,21 @@ +import { siteConfig } from "@/config/site"; + +export function GET() { + const xml = ` + + + ${siteConfig.url} + daily + 1 + + + ${siteConfig.url}/about + monthly + 0.8 + +`; + + return new Response(xml, { + headers: { "Content-Type": "application/xml" }, + }); +} diff --git a/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.module.css b/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.module.css new file mode 100644 index 0000000..b6308ef --- /dev/null +++ b/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.module.css @@ -0,0 +1,13 @@ +.headerFooter { + margin: -32px -32px 16px -32px; + padding: 0 32px; + border: 1px var(--BenWard-bodyTextColor) solid; + border-width: 0 0 1px 0; +} + +.footer { + margin: 16px -32px -32px -32px; + padding: 0 32px; + border: 1px var(--BenWard-bodyTextColor) solid; + border-width: 1px 0 0 0; +} diff --git a/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.tsx b/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.tsx new file mode 100644 index 0000000..5aa6ca8 --- /dev/null +++ b/nextjs/components/ArchiveMonthLayout/ArchiveMonthLayout.tsx @@ -0,0 +1,39 @@ +import type { MonthArchive } from "@/lib/archives"; +import { monthName } from "@/lib/dates"; +import { ArchiveNavigation } from "@/components/ArchiveNavigation"; +import { PostSummary } from "@/components/PostSummary"; +import styles from "./ArchiveMonthLayout.module.css"; + +interface ArchiveMonthLayoutProps { + archive: MonthArchive; +} + +export function ArchiveMonthLayout({ archive }: ArchiveMonthLayoutProps) { + const { period, posts } = archive; + const displayMonth = period.month ? monthName(period.month) : ""; + const dateTime = `${period.year}-${String(period.month).padStart(2, "0")}`; + + return ( +
+
+ +
+

+ Posts from{" "} + +

+
    + {posts.map((post) => ( +
  • + +
  • + ))} +
+
+ +
+
+ ); +} diff --git a/nextjs/components/ArchiveMonthLayout/index.ts b/nextjs/components/ArchiveMonthLayout/index.ts new file mode 100644 index 0000000..69f0eba --- /dev/null +++ b/nextjs/components/ArchiveMonthLayout/index.ts @@ -0,0 +1 @@ +export { ArchiveMonthLayout } from "./ArchiveMonthLayout"; diff --git a/nextjs/components/ArchiveNavigation/ArchiveNavigation.module.css b/nextjs/components/ArchiveNavigation/ArchiveNavigation.module.css new file mode 100644 index 0000000..10d5dcc --- /dev/null +++ b/nextjs/components/ArchiveNavigation/ArchiveNavigation.module.css @@ -0,0 +1,25 @@ +.nav { + display: flex; + justify-content: space-between; +} + +.nav a { + display: block; + text-align: center; + margin: 16px 0; + white-space: nowrap; + flex-basis: 33%; +} + +.nav a:first-child { + text-align: left; +} + +.nav a:last-child { + text-align: right; +} + +.navAnnual a, +.navRecent a { + flex-basis: 50%; +} diff --git a/nextjs/components/ArchiveNavigation/ArchiveNavigation.test.tsx b/nextjs/components/ArchiveNavigation/ArchiveNavigation.test.tsx new file mode 100644 index 0000000..8400a70 --- /dev/null +++ b/nextjs/components/ArchiveNavigation/ArchiveNavigation.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import React from "react"; +import { render } from "@testing-library/react"; +import { ArchiveNavigation } from "./ArchiveNavigation"; +import type { ArchiveNavigation as ArchiveNav } from "@/lib/archives"; + +describe("ArchiveNavigation", () => { + const nav: ArchiveNav = { + period: { year: 2022, month: 6 }, + previousPeriod: { year: 2022, month: 3 }, + nextPeriod: { year: 2022, month: 12 }, + firstPeriod: { year: 2020, month: 1 }, + lastPeriod: { year: 2022, month: 12 }, + }; + + it("renders previous and next links", () => { + const { container } = render(); + const links = container.querySelectorAll("a"); + const prevLink = Array.from(links).find((l) => l.rel === "prev"); + const nextLink = Array.from(links).find((l) => l.rel === "next"); + expect(prevLink?.getAttribute("href")).toBe("/2022/03"); + expect(nextLink?.getAttribute("href")).toBe("/2022/12"); + }); + + it("renders parent link for monthly archives", () => { + const { container } = render(); + const parentLink = container.querySelector('a[rel="parent"]'); + expect(parentLink?.getAttribute("href")).toBe("/2022"); + }); + + it("shows last period when no previous", () => { + const firstNav: ArchiveNav = { + ...nav, + previousPeriod: null, + }; + const { container } = render(); + const lastLink = container.querySelector('a[rel="last"]'); + expect(lastLink).toBeTruthy(); + }); + + it("shows first period when no next", () => { + const lastNav: ArchiveNav = { + ...nav, + nextPeriod: null, + }; + const { container } = render(); + const firstLink = container.querySelector('a[rel="first"]'); + expect(firstLink).toBeTruthy(); + }); +}); diff --git a/nextjs/components/ArchiveNavigation/ArchiveNavigation.tsx b/nextjs/components/ArchiveNavigation/ArchiveNavigation.tsx new file mode 100644 index 0000000..9aec145 --- /dev/null +++ b/nextjs/components/ArchiveNavigation/ArchiveNavigation.tsx @@ -0,0 +1,49 @@ +import type { ArchiveNavigation as ArchiveNav } from "@/lib/archives"; +import { archiveUrl, formatArchivePeriod } from "@/lib/archives"; +import styles from "./ArchiveNavigation.module.css"; + +interface ArchiveNavigationProps { + navigation: ArchiveNav; + isAnnual?: boolean; +} + +export function ArchiveNavigation({ + navigation, + isAnnual, +}: ArchiveNavigationProps) { + const { period, previousPeriod, nextPeriod, firstPeriod, lastPeriod } = + navigation; + const isMonthly = !!period.month; + + return ( + + ); +} diff --git a/nextjs/components/ArchiveNavigation/index.ts b/nextjs/components/ArchiveNavigation/index.ts new file mode 100644 index 0000000..2a0599a --- /dev/null +++ b/nextjs/components/ArchiveNavigation/index.ts @@ -0,0 +1 @@ +export { ArchiveNavigation } from "./ArchiveNavigation"; diff --git a/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.module.css b/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.module.css new file mode 100644 index 0000000..69772e8 --- /dev/null +++ b/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.module.css @@ -0,0 +1,18 @@ +.headerFooter { + margin: -32px -32px 16px -32px; + padding: 0 32px; + border: 1px var(--BenWard-bodyTextColor) solid; + border-width: 0 0 1px 0; +} + +.footer { + margin: 16px -32px -32px -32px; + padding: 0 32px; + border: 1px var(--BenWard-bodyTextColor) solid; + border-width: 1px 0 0 0; +} + +.monthSection article { + margin-left: 24px; + position: relative; +} diff --git a/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.tsx b/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.tsx new file mode 100644 index 0000000..32d7a2a --- /dev/null +++ b/nextjs/components/ArchiveYearLayout/ArchiveYearLayout.tsx @@ -0,0 +1,67 @@ +import type { YearArchive } from "@/lib/archives"; +import { formatDisplay, formatTime } from "@/lib/dates"; +import { ArchiveNavigation } from "@/components/ArchiveNavigation"; +import styles from "./ArchiveYearLayout.module.css"; + +interface ArchiveYearLayoutProps { + archive: YearArchive; +} + +export function ArchiveYearLayout({ archive }: ArchiveYearLayoutProps) { + const { period, months } = archive; + + return ( +
+
+ +
+

+ Posts from +

+ {months.map(({ month, name, posts: monthPosts }) => ( +
+

{name}

+
    + {monthPosts.map((post) => ( +
  • +
    +

    + + {post.frontmatter.title} + +

    +

    + +

    + {post.excerpt && ( +
    + )} +
    +
  • + ))} +
+
+ ))} +
+ +
+
+ ); +} diff --git a/nextjs/components/ArchiveYearLayout/index.ts b/nextjs/components/ArchiveYearLayout/index.ts new file mode 100644 index 0000000..d110eea --- /dev/null +++ b/nextjs/components/ArchiveYearLayout/index.ts @@ -0,0 +1 @@ +export { ArchiveYearLayout } from "./ArchiveYearLayout"; diff --git a/nextjs/components/ArticleLayout/ArticleLayout.tsx b/nextjs/components/ArticleLayout/ArticleLayout.tsx new file mode 100644 index 0000000..4013949 --- /dev/null +++ b/nextjs/components/ArticleLayout/ArticleLayout.tsx @@ -0,0 +1,7 @@ +interface ArticleLayoutProps { + content: string; +} + +export function ArticleLayout({ content }: ArticleLayoutProps) { + return
; +} diff --git a/nextjs/components/ArticleLayout/index.ts b/nextjs/components/ArticleLayout/index.ts new file mode 100644 index 0000000..6bac0e4 --- /dev/null +++ b/nextjs/components/ArticleLayout/index.ts @@ -0,0 +1 @@ +export { ArticleLayout } from "./ArticleLayout"; diff --git a/nextjs/components/BlogPostLayout/BlogPostLayout.module.css b/nextjs/components/BlogPostLayout/BlogPostLayout.module.css new file mode 100644 index 0000000..95d5b4a --- /dev/null +++ b/nextjs/components/BlogPostLayout/BlogPostLayout.module.css @@ -0,0 +1,71 @@ +.dateline { + color: var(--BenWard-accentColor); + margin: 6px 0; + font-size: 16px; + font-weight: 500; + transform: rotate(1deg) translateX(-10px) translateY(3px); +} + +.dateline p { + margin: 0; +} + +.title { + transform: rotate(-1deg) translateX(-3px); +} + +.tags { + padding: 0; +} + +.tags li { + margin: 0; + padding: 0; + display: inline-block; + list-style: none; + padding: 4px; + border-radius: 4px; + border-bottom-left-radius: 15px 50%; + border-top-left-radius: 15px 50%; + padding-left: 10px; + transform: rotate(-5deg); + border: 2px solid var(--BenWard-accentColor); + color: var(--BenWard-accentColor); + font-size: 80%; + margin-bottom: 4px; + line-height: 120%; +} + +.tags li a { + display: inline-block; + color: inherit; +} + +/* Article header/footer navigation */ +.navFooter { + margin: 16px -32px -32px -32px; + padding: 0 32px; + border: 1px var(--BenWard-bodyTextColor) solid; + border-width: 1px 0 0 0; +} + +.nav { + display: flex; + justify-content: space-between; +} + +.nav a { + display: block; + text-align: center; + margin: 16px 0; + white-space: nowrap; + flex-basis: 33%; +} + +.nav a:first-child { + text-align: left; +} + +.nav a:last-child { + text-align: right; +} diff --git a/nextjs/components/BlogPostLayout/BlogPostLayout.tsx b/nextjs/components/BlogPostLayout/BlogPostLayout.tsx new file mode 100644 index 0000000..dba6d24 --- /dev/null +++ b/nextjs/components/BlogPostLayout/BlogPostLayout.tsx @@ -0,0 +1,100 @@ +import type { EnrichedPost } from "@/lib/posts"; +import { formatDisplay, formatArchivePath, formatMonthYear } from "@/lib/dates"; +import { Share } from "@/components/Share"; +import styles from "./BlogPostLayout.module.css"; + +interface BlogPostLayoutProps { + post: EnrichedPost; + previousPost: EnrichedPost | null; + nextPost: EnrichedPost | null; +} + +export function BlogPostLayout({ + post, + previousPost, + nextPost, +}: BlogPostLayoutProps) { + const archivePath = formatArchivePath(post.globalDate); + const archiveLabel = formatMonthYear(post.globalDate); + + return ( +
+

{post.frontmatter.title}

+
+

+ + . + {post.frontmatter.updated && ( + <> + {" "} + Updated:{" "} + + . + + )} +

+ {post.frontmatter.geo?.name && ( +

+ {post.frontmatter.geo.xy && ( + + )} + {post.frontmatter.geo.name} +

+ )} +
+
+ {post.frontmatter.tags && post.frontmatter.tags.length > 0 && ( +
    + {post.frontmatter.tags.map((tag) => ( +
  • + +
  • + ))} +
+ )} + + +
+ ); +} diff --git a/nextjs/components/BlogPostLayout/index.ts b/nextjs/components/BlogPostLayout/index.ts new file mode 100644 index 0000000..92eb687 --- /dev/null +++ b/nextjs/components/BlogPostLayout/index.ts @@ -0,0 +1 @@ +export { BlogPostLayout } from "./BlogPostLayout"; diff --git a/nextjs/components/Cover/Cover.module.css b/nextjs/components/Cover/Cover.module.css new file mode 100644 index 0000000..58d684d --- /dev/null +++ b/nextjs/components/Cover/Cover.module.css @@ -0,0 +1,28 @@ +.cover { + margin: -32px -32px 16px -32px; + height: 700px; + background: transparent url('https://media.benward.uk/web/yosemite-header.webp') no-repeat 0 0; + background-size: cover; + position: relative; + box-shadow: inset 0 0 48px rgba(0, 0, 0, 0.5); +} + +.cover p { + position: absolute; + bottom: 0; left: 0; + margin: 0 0 8px 0; + padding: 8px 32px; + background: rgba(0,0,0,0.5); + color: var(--BenWard-asideTextColor); +} + +.cover a:link, +.cover a:visited { + color: var(--BenWard-asideTextColor); + text-decoration: underline; +} + +.cover a:hover { + background: var(--BenWard-pageColor); + color: var(--BenWard-accentColor); +} diff --git a/nextjs/components/Cover/Cover.test.tsx b/nextjs/components/Cover/Cover.test.tsx new file mode 100644 index 0000000..ecbfca6 --- /dev/null +++ b/nextjs/components/Cover/Cover.test.tsx @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import React from "react"; +import { render } from "@testing-library/react"; +import { Cover } from "./Cover"; + +describe("Cover", () => { + it("renders h-card microformat", () => { + const { container } = render(); + expect(container.querySelector(".h-card")).toBeTruthy(); + }); + + it("includes p-name link", () => { + const { container } = render(); + const nameLink = container.querySelector(".p-name.u-url"); + expect(nameLink).toBeTruthy(); + expect(nameLink?.textContent).toBe("Ben Ward"); + }); + + it("has about link", () => { + const { container } = render(); + const aboutLink = container.querySelector('a[href="/about"]'); + expect(aboutLink).toBeTruthy(); + }); +}); diff --git a/nextjs/components/Cover/Cover.tsx b/nextjs/components/Cover/Cover.tsx new file mode 100644 index 0000000..9000daa --- /dev/null +++ b/nextjs/components/Cover/Cover.tsx @@ -0,0 +1,34 @@ +import styles from "./Cover.module.css"; + +export function Cover() { + return ( + + ); +} diff --git a/nextjs/components/Cover/index.ts b/nextjs/components/Cover/index.ts new file mode 100644 index 0000000..0e03ba5 --- /dev/null +++ b/nextjs/components/Cover/index.ts @@ -0,0 +1 @@ +export { Cover } from "./Cover"; diff --git a/nextjs/components/Identity/Identity.tsx b/nextjs/components/Identity/Identity.tsx new file mode 100644 index 0000000..715edfe --- /dev/null +++ b/nextjs/components/Identity/Identity.tsx @@ -0,0 +1,9 @@ +export function Identity() { + return ( + + ); +} diff --git a/nextjs/components/Identity/index.ts b/nextjs/components/Identity/index.ts new file mode 100644 index 0000000..cecfd8d --- /dev/null +++ b/nextjs/components/Identity/index.ts @@ -0,0 +1 @@ +export { Identity } from "./Identity"; diff --git a/nextjs/components/PostSummary/PostSummary.module.css b/nextjs/components/PostSummary/PostSummary.module.css new file mode 100644 index 0000000..c2dfe1b --- /dev/null +++ b/nextjs/components/PostSummary/PostSummary.module.css @@ -0,0 +1,10 @@ +.article { + margin-bottom: 32px; +} + +.dateline { + color: var(--BenWard-accentColor); + margin: 6px 0; + font-size: 16px; + font-weight: 500; +} diff --git a/nextjs/components/PostSummary/PostSummary.test.tsx b/nextjs/components/PostSummary/PostSummary.test.tsx new file mode 100644 index 0000000..10f2602 --- /dev/null +++ b/nextjs/components/PostSummary/PostSummary.test.tsx @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import React from "react"; +import { render } from "@testing-library/react"; +import { PostSummary } from "./PostSummary"; +import type { EnrichedPost } from "@/lib/posts"; + +function makePost(overrides: Partial = {}): EnrichedPost { + return { + slug: "test-post", + filePath: "", + relativePath: "", + frontmatter: { + layout: "blog", + category: "blog", + title: "Test Post Title", + date: "2022-12-07T00:22:52-08:00", + tags: ["test"], + }, + content: "", + rawContent: "", + format: "markdown", + cleanUrl: "/blog/test-post", + githubSourceUrl: "", + globalDate: "2022-12-07T00:22:52-08:00", + dateTitle: false, + excerpt: "", + ...overrides, + }; +} + +describe("PostSummary", () => { + it("renders h-entry microformat", () => { + const { container } = render(); + expect(container.querySelector(".h-entry")).toBeTruthy(); + }); + + it("renders post title as link", () => { + const { container } = render(); + const link = container.querySelector("a.u-url"); + expect(link?.textContent).toBe("Test Post Title"); + expect(link?.getAttribute("href")).toBe("/blog/test-post"); + }); + + it("uses canonical URL when available", () => { + const post = makePost({ + frontmatter: { + ...makePost().frontmatter, + canonical: "https://example.com/review", + }, + }); + const { container } = render(); + const link = container.querySelector("a.u-url"); + expect(link?.getAttribute("href")).toBe("https://example.com/review"); + }); + + it("renders dt-published time", () => { + const { container } = render(); + const time = container.querySelector(".dt-published"); + expect(time).toBeTruthy(); + expect(time?.getAttribute("datetime")).toBe("2022-12-07T00:22:52-08:00"); + }); + + it("renders excerpt when present", () => { + const post = makePost({ excerpt: "A brief summary" }); + const { container } = render(); + expect(container.querySelector(".e-summary")?.textContent).toBe( + "A brief summary" + ); + }); + + it("omits excerpt div when empty", () => { + const { container } = render(); + expect(container.querySelector(".e-summary")).toBeNull(); + }); +}); diff --git a/nextjs/components/PostSummary/PostSummary.tsx b/nextjs/components/PostSummary/PostSummary.tsx new file mode 100644 index 0000000..f24cc88 --- /dev/null +++ b/nextjs/components/PostSummary/PostSummary.tsx @@ -0,0 +1,39 @@ +import type { EnrichedPost } from "@/lib/posts"; +import { formatDisplay, formatTime } from "@/lib/dates"; +import styles from "./PostSummary.module.css"; + +interface PostSummaryProps { + post: EnrichedPost; +} + +export function PostSummary({ post }: PostSummaryProps) { + const url = post.frontmatter.canonical || post.cleanUrl; + const timeDisplay = post.dateTitle + ? formatTime(post.globalDate) + : formatDisplay(post.globalDate); + + return ( + + ); +} diff --git a/nextjs/components/PostSummary/index.ts b/nextjs/components/PostSummary/index.ts new file mode 100644 index 0000000..40b4dc1 --- /dev/null +++ b/nextjs/components/PostSummary/index.ts @@ -0,0 +1 @@ +export { PostSummary } from "./PostSummary"; diff --git a/nextjs/components/Profiles/Profiles.tsx b/nextjs/components/Profiles/Profiles.tsx new file mode 100644 index 0000000..8586571 --- /dev/null +++ b/nextjs/components/Profiles/Profiles.tsx @@ -0,0 +1,16 @@ +export function Profiles() { + return ( + <> + + + + ); +} diff --git a/nextjs/components/Profiles/index.ts b/nextjs/components/Profiles/index.ts new file mode 100644 index 0000000..39921ba --- /dev/null +++ b/nextjs/components/Profiles/index.ts @@ -0,0 +1 @@ +export { Profiles } from "./Profiles"; diff --git a/nextjs/components/Scripts/Scripts.tsx b/nextjs/components/Scripts/Scripts.tsx new file mode 100644 index 0000000..3bfd2be --- /dev/null +++ b/nextjs/components/Scripts/Scripts.tsx @@ -0,0 +1,37 @@ +"use client"; + +import Script from "next/script"; +import { siteConfig } from "@/config/site"; + +export function Scripts() { + return ( + <> + + + + ); +} diff --git a/nextjs/components/Scripts/index.ts b/nextjs/components/Scripts/index.ts new file mode 100644 index 0000000..0814c9c --- /dev/null +++ b/nextjs/components/Scripts/index.ts @@ -0,0 +1 @@ +export { Scripts } from "./Scripts"; diff --git a/nextjs/components/Share/Share.module.css b/nextjs/components/Share/Share.module.css new file mode 100644 index 0000000..5e8a03f --- /dev/null +++ b/nextjs/components/Share/Share.module.css @@ -0,0 +1,11 @@ +.links { + position: relative; +} + +.links p { + font-size: 90%; +} + +.followUp p { + font-size: 90%; +} diff --git a/nextjs/components/Share/Share.tsx b/nextjs/components/Share/Share.tsx new file mode 100644 index 0000000..b9a5817 --- /dev/null +++ b/nextjs/components/Share/Share.tsx @@ -0,0 +1,56 @@ +import { siteConfig } from "@/config/site"; +import { shortlink } from "@/lib/base60"; +import styles from "./Share.module.css"; + +interface ShareProps { + cleanUrl: string; + globalDate: string; + githubSourceUrl: string; +} + +export function Share({ cleanUrl, globalDate, githubSourceUrl }: ShareProps) { + const permalink = `${siteConfig.url}${cleanUrl}`; + const shortUrl = shortlink(globalDate, siteConfig.shortdomain); + + return ( + <> +
+

Links

+

+ To share this entry, or reference it in commentary of your own, link + to the following: +

+ +
+
+

+ You can file issues or provide corrections:{" "} + + View Source on Github + + .{" "} + + Contributor credits. + +

+
+ + ); +} diff --git a/nextjs/components/Share/index.ts b/nextjs/components/Share/index.ts new file mode 100644 index 0000000..ebca8e7 --- /dev/null +++ b/nextjs/components/Share/index.ts @@ -0,0 +1 @@ +export { Share } from "./Share"; diff --git a/nextjs/components/TwitterMeta/TwitterMeta.tsx b/nextjs/components/TwitterMeta/TwitterMeta.tsx new file mode 100644 index 0000000..27f8379 --- /dev/null +++ b/nextjs/components/TwitterMeta/TwitterMeta.tsx @@ -0,0 +1,21 @@ +import { siteConfig } from "@/config/site"; + +interface TwitterMetaProps { + title?: string; +} + +export function TwitterMeta({ title }: TwitterMetaProps) { + const displayTitle = title || siteConfig.title; + return ( + <> + + + + + + + ); +} diff --git a/nextjs/components/TwitterMeta/index.ts b/nextjs/components/TwitterMeta/index.ts new file mode 100644 index 0000000..5919b47 --- /dev/null +++ b/nextjs/components/TwitterMeta/index.ts @@ -0,0 +1 @@ +export { TwitterMeta } from "./TwitterMeta"; diff --git a/nextjs/config/site.test.ts b/nextjs/config/site.test.ts new file mode 100644 index 0000000..dfc74fc --- /dev/null +++ b/nextjs/config/site.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { siteConfig } from "./site"; + +describe("siteConfig", () => { + it("has required site metadata", () => { + expect(siteConfig.title).toBe("Ben Ward"); + expect(siteConfig.url).toBe("https://benward.uk"); + expect(siteConfig.shortdomain).toBe("https://bnwrd.me"); + }); + + it("has author info", () => { + expect(siteConfig.author.name).toBe("Ben Ward"); + expect(siteConfig.author.twitter).toBe("benward"); + }); + + it("has GitHub configuration", () => { + expect(siteConfig.githubSlug).toBe("BenWard/benward-web"); + expect(siteConfig.gitBase).toBe("jekyll"); + }); + + it("has date format strings", () => { + expect(siteConfig.dateFormats.iso).toBeDefined(); + expect(siteConfig.dateFormats.display).toBeDefined(); + expect(siteConfig.dateFormats.time).toBeDefined(); + }); +}); diff --git a/nextjs/config/site.ts b/nextjs/config/site.ts new file mode 100644 index 0000000..e3972fc --- /dev/null +++ b/nextjs/config/site.ts @@ -0,0 +1,23 @@ +export const siteConfig = { + title: "Ben Ward", + url: "https://benward.uk", + shortdomain: "https://bnwrd.me", + githubSlug: "BenWard/benward-web", + gitBase: "jekyll", + author: { + name: "Ben Ward", + url: "https://benward.uk", + twitter: "benward", + twitterId: "12249", + }, + dateFormats: { + iso: "yyyy-MM-dd'T'HH:mm:ssxxx", + display: "MMM d yyyy, H:mm (xxx)", + time: "H:mm (xxx)", + }, + /** Domain switched in 2018 */ + domainCutoverDate: "2018-01-01", + legacyDomain: "benward.me", + currentDomain: "benward.uk", + gaugesSiteId: "515a7c52108d7b061f000024", +} as const; diff --git a/nextjs/lib/archives.test.ts b/nextjs/lib/archives.test.ts new file mode 100644 index 0000000..b710796 --- /dev/null +++ b/nextjs/lib/archives.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect } from "vitest"; +import { + getYearPeriods, + getMonthPeriods, + getYearArchive, + getMonthArchive, + archiveUrl, + formatArchivePeriod, +} from "./archives"; +import type { EnrichedPost } from "./posts"; + +function makeEnrichedPost( + date: string, + slug: string = "test" +): EnrichedPost { + return { + slug, + filePath: "", + relativePath: "", + frontmatter: { + layout: "blog", + category: "blog", + title: `Post ${slug}`, + date, + tags: [], + }, + content: "", + rawContent: "", + format: "markdown", + cleanUrl: `/blog/${slug}`, + githubSourceUrl: "", + globalDate: date, + dateTitle: false, + excerpt: "", + }; +} + +const posts: EnrichedPost[] = [ + makeEnrichedPost("2022-12-07T00:00:00+00:00", "dec-post"), + makeEnrichedPost("2022-06-15T00:00:00+00:00", "jun-post"), + makeEnrichedPost("2021-03-01T00:00:00+00:00", "mar-post"), + makeEnrichedPost("2020-01-10T00:00:00+00:00", "jan-post"), +]; + +describe("getYearPeriods", () => { + it("returns unique years sorted ascending", () => { + const periods = getYearPeriods(posts); + expect(periods).toEqual([{ year: 2020 }, { year: 2021 }, { year: 2022 }]); + }); +}); + +describe("getMonthPeriods", () => { + it("returns unique year-month pairs sorted ascending", () => { + const periods = getMonthPeriods(posts); + expect(periods).toEqual([ + { year: 2020, month: 1 }, + { year: 2021, month: 3 }, + { year: 2022, month: 6 }, + { year: 2022, month: 12 }, + ]); + }); +}); + +describe("getYearArchive", () => { + it("returns archive for a valid year", () => { + const archive = getYearArchive(posts, 2022); + expect(archive).not.toBeNull(); + expect(archive!.posts).toHaveLength(2); + expect(archive!.months).toHaveLength(2); + }); + + it("has correct navigation", () => { + const archive = getYearArchive(posts, 2021); + expect(archive!.previousPeriod).toEqual({ year: 2020 }); + expect(archive!.nextPeriod).toEqual({ year: 2022 }); + }); + + it("first year has no previous", () => { + const archive = getYearArchive(posts, 2020); + expect(archive!.previousPeriod).toBeNull(); + expect(archive!.nextPeriod).toEqual({ year: 2021 }); + }); + + it("last year has no next", () => { + const archive = getYearArchive(posts, 2022); + expect(archive!.nextPeriod).toBeNull(); + }); + + it("returns null for non-existent year", () => { + expect(getYearArchive(posts, 2019)).toBeNull(); + }); + + it("groups posts by month", () => { + const archive = getYearArchive(posts, 2022); + expect(archive!.months[0].name).toBe("June"); + expect(archive!.months[1].name).toBe("December"); + }); +}); + +describe("getMonthArchive", () => { + it("returns archive for a valid month", () => { + const archive = getMonthArchive(posts, 2022, 12); + expect(archive).not.toBeNull(); + expect(archive!.posts).toHaveLength(1); + }); + + it("has correct navigation", () => { + const archive = getMonthArchive(posts, 2022, 6); + expect(archive!.previousPeriod).toEqual({ year: 2021, month: 3 }); + expect(archive!.nextPeriod).toEqual({ year: 2022, month: 12 }); + }); + + it("returns null for non-existent month", () => { + expect(getMonthArchive(posts, 2022, 3)).toBeNull(); + }); +}); + +describe("archiveUrl", () => { + it("formats year-only period", () => { + expect(archiveUrl({ year: 2022 })).toBe("/2022"); + }); + + it("formats year-month period with zero-padded month", () => { + expect(archiveUrl({ year: 2022, month: 6 })).toBe("/2022/06"); + expect(archiveUrl({ year: 2022, month: 12 })).toBe("/2022/12"); + }); +}); + +describe("formatArchivePeriod", () => { + it("formats year-only", () => { + expect(formatArchivePeriod({ year: 2022 })).toBe("2022"); + }); + + it("formats year-month", () => { + expect(formatArchivePeriod({ year: 2022, month: 6 })).toBe("June 2022"); + }); +}); diff --git a/nextjs/lib/archives.ts b/nextjs/lib/archives.ts new file mode 100644 index 0000000..42d9828 --- /dev/null +++ b/nextjs/lib/archives.ts @@ -0,0 +1,175 @@ +/** + * Archive generation. + * Port of the Jekyll archives.rb plugin. + * Groups posts by year and month, with pagination between periods. + */ + +import type { EnrichedPost } from "./posts"; +import { monthName } from "./dates"; + +export interface ArchivePeriod { + year: number; + month?: number; +} + +export interface ArchiveNavigation { + period: ArchivePeriod; + previousPeriod: ArchivePeriod | null; + nextPeriod: ArchivePeriod | null; + firstPeriod: ArchivePeriod | null; + lastPeriod: ArchivePeriod | null; +} + +export interface YearArchive extends ArchiveNavigation { + posts: EnrichedPost[]; + months: { month: number; name: string; posts: EnrichedPost[] }[]; +} + +export interface MonthArchive extends ArchiveNavigation { + posts: EnrichedPost[]; +} + +/** + * Get year and month from a post's date string. + */ +function postYearMonth(post: EnrichedPost): { year: number; month: number } { + const date = new Date(post.frontmatter.date); + return { year: date.getFullYear(), month: date.getMonth() + 1 }; +} + +/** + * Get all unique year periods, sorted ascending. + */ +export function getYearPeriods(posts: EnrichedPost[]): ArchivePeriod[] { + const years = new Set(); + for (const post of posts) { + years.add(postYearMonth(post).year); + } + return Array.from(years) + .sort((a, b) => a - b) + .map((year) => ({ year })); +} + +/** + * Get all unique month periods, sorted ascending. + */ +export function getMonthPeriods(posts: EnrichedPost[]): ArchivePeriod[] { + const seen = new Set(); + const periods: ArchivePeriod[] = []; + + for (const post of posts) { + const { year, month } = postYearMonth(post); + const key = `${year}-${month}`; + if (!seen.has(key)) { + seen.add(key); + periods.push({ year, month }); + } + } + + return periods.sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return (a.month || 0) - (b.month || 0); + }); +} + +/** + * Build navigation for a period within a sorted list. + */ +function buildNavigation( + periods: ArchivePeriod[], + index: number +): Omit { + return { + previousPeriod: index > 0 ? periods[index - 1] : null, + nextPeriod: index < periods.length - 1 ? periods[index + 1] : null, + firstPeriod: periods[0] || null, + lastPeriod: periods[periods.length - 1] || null, + }; +} + +/** + * Get the archive data for a specific year. + */ +export function getYearArchive( + posts: EnrichedPost[], + year: number +): YearArchive | null { + const yearPosts = posts.filter( + (p) => postYearMonth(p).year === year + ); + if (yearPosts.length === 0) return null; + + const yearPeriods = getYearPeriods(posts); + const index = yearPeriods.findIndex((p) => p.year === year); + if (index === -1) return null; + + // Group by month + const monthMap = new Map(); + for (const post of yearPosts) { + const { month } = postYearMonth(post); + if (!monthMap.has(month)) monthMap.set(month, []); + monthMap.get(month)!.push(post); + } + + const months = Array.from(monthMap.entries()) + .sort(([a], [b]) => a - b) + .map(([month, monthPosts]) => ({ + month, + name: monthName(month), + posts: monthPosts, + })); + + return { + period: { year }, + ...buildNavigation(yearPeriods, index), + posts: yearPosts, + months, + }; +} + +/** + * Get the archive data for a specific month. + */ +export function getMonthArchive( + posts: EnrichedPost[], + year: number, + month: number +): MonthArchive | null { + const monthPosts = posts.filter((p) => { + const ym = postYearMonth(p); + return ym.year === year && ym.month === month; + }); + if (monthPosts.length === 0) return null; + + const monthPeriods = getMonthPeriods(posts); + const index = monthPeriods.findIndex( + (p) => p.year === year && p.month === month + ); + if (index === -1) return null; + + return { + period: { year, month }, + ...buildNavigation(monthPeriods, index), + posts: monthPosts, + }; +} + +/** + * Format an archive period as a URL path. + */ +export function archiveUrl(period: ArchivePeriod): string { + if (period.month) { + return `/${period.year}/${period.month.toString().padStart(2, "0")}`; + } + return `/${period.year}`; +} + +/** + * Format an archive period for display. + */ +export function formatArchivePeriod(period: ArchivePeriod): string { + if (period.month) { + return `${monthName(period.month)} ${period.year}`; + } + return `${period.year}`; +} diff --git a/nextjs/lib/base60.test.ts b/nextjs/lib/base60.test.ts new file mode 100644 index 0000000..8f34366 --- /dev/null +++ b/nextjs/lib/base60.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { encodeBase60, shortlink } from "./base60"; + +describe("encodeBase60", () => { + it("encodes 0", () => { + expect(encodeBase60(0)).toBe("0"); + }); + + it("encodes small numbers", () => { + expect(encodeBase60(1)).toBe("1"); + expect(encodeBase60(59)).toBe("z"); + expect(encodeBase60(60)).toBe("10"); + }); + + it("encodes Unix timestamps", () => { + // 2022-12-07T00:22:52-08:00 = 1670397772 + const result = encodeBase60(1670397772); + expect(result).toBeTruthy(); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + it("only uses valid NewBase60 characters", () => { + const valid = /^[0-9A-HJ-NP-Za-km-z_]+$/; + expect(encodeBase60(1670397772)).toMatch(valid); + expect(encodeBase60(999999999)).toMatch(valid); + }); +}); + +describe("shortlink", () => { + it("generates shortlink from date and domain", () => { + const result = shortlink("2022-12-07T00:22:52-08:00", "https://bnwrd.me"); + expect(result).toMatch(/^https:\/\/bnwrd\.me\//); + expect(result.length).toBeGreaterThan("https://bnwrd.me/".length); + }); +}); diff --git a/nextjs/lib/base60.ts b/nextjs/lib/base60.ts new file mode 100644 index 0000000..4ca1c3f --- /dev/null +++ b/nextjs/lib/base60.ts @@ -0,0 +1,26 @@ +/** + * NewBase60 encoding for shortlink generation. + * Port of the Ruby new_base_60 gem. + * See: http://tantek.pbworks.com/w/page/19402946/NewBase60 + */ + +const CHARSET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"; + +export function encodeBase60(n: number): string { + if (n === 0) return "0"; + + let num = Math.floor(n); + let result = ""; + + while (num > 0) { + result = CHARSET[num % 60] + result; + num = Math.floor(num / 60); + } + + return result; +} + +export function shortlink(dateStr: string, shortdomain: string): string { + const timestamp = Math.floor(new Date(dateStr).getTime() / 1000); + return `${shortdomain}/${encodeBase60(timestamp)}`; +} diff --git a/nextjs/lib/content.test.ts b/nextjs/lib/content.test.ts new file mode 100644 index 0000000..48d9122 --- /dev/null +++ b/nextjs/lib/content.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from "vitest"; +import { + parsePostFilename, + renderMarkdown, + renderTextile, + loadAllPostsSync, + loadPostSync, +} from "./content"; +import fs from "fs"; +import path from "path"; + +describe("parsePostFilename", () => { + it("parses markdown filenames", () => { + const result = parsePostFilename("2022-12-07-my-post.md"); + expect(result).toEqual({ date: "2022-12-07", slug: "my-post" }); + }); + + it("parses textile filenames", () => { + const result = parsePostFilename("2005-01-04-old-post.textile"); + expect(result).toEqual({ date: "2005-01-04", slug: "old-post" }); + }); + + it("handles slugs with hyphens", () => { + const result = parsePostFilename( + "2009-02-15-a-long-slug-name.md" + ); + expect(result).toEqual({ + date: "2009-02-15", + slug: "a-long-slug-name", + }); + }); + + it("returns null for invalid filenames", () => { + expect(parsePostFilename("not-a-post.txt")).toBeNull(); + expect(parsePostFilename("readme.md")).toBeNull(); + }); +}); + +describe("renderMarkdown", () => { + it("renders basic markdown to HTML", async () => { + const html = await renderMarkdown("# Hello\n\nA paragraph."); + expect(html).toContain("

Hello

"); + expect(html).toContain("

A paragraph.

"); + }); + + it("preserves raw HTML in markdown", async () => { + const html = await renderMarkdown( + '
content
' + ); + expect(html).toContain('
content
'); + }); + + it("renders links", async () => { + const html = await renderMarkdown("[example](https://example.com)"); + expect(html).toContain('example'); + }); +}); + +describe("renderTextile", () => { + it("renders basic textile to HTML", () => { + const html = renderTextile("h1. Hello\n\nA paragraph."); + expect(html).toContain("Hello"); + expect(html).toContain("A paragraph"); + }); +}); + +describe("loadAllPostsSync", () => { + it("loads posts from the Jekyll posts directory", () => { + const posts = loadAllPostsSync(); + expect(posts.length).toBeGreaterThan(0); + }); + + it("posts are sorted by date descending", () => { + const posts = loadAllPostsSync(); + for (let i = 1; i < posts.length; i++) { + const prev = new Date(posts[i - 1].frontmatter.date).getTime(); + const curr = new Date(posts[i].frontmatter.date).getTime(); + expect(prev).toBeGreaterThanOrEqual(curr); + } + }); + + it("includes both markdown and textile posts", () => { + const posts = loadAllPostsSync(); + const formats = new Set(posts.map((p) => p.format)); + expect(formats.has("markdown")).toBe(true); + expect(formats.has("textile")).toBe(true); + }); +}); + +describe("loadPostSync", () => { + it("loads a real post file", () => { + const postsDir = path.join( + process.cwd(), + "..", + "jekyll", + "_posts", + "blog" + ); + // Find any markdown file + const years = fs.readdirSync(postsDir); + let testFile: string | null = null; + for (const year of years) { + const yearDir = path.join(postsDir, year); + const stat = fs.statSync(yearDir); + if (!stat.isDirectory()) continue; + const files = fs.readdirSync(yearDir); + const mdFile = files.find((f: string) => f.endsWith(".md")); + if (mdFile) { + testFile = path.join(yearDir, mdFile); + break; + } + } + + if (testFile) { + const post = loadPostSync(testFile); + expect(post).not.toBeNull(); + expect(post!.frontmatter.date).toBeDefined(); + expect(post!.slug).toBeTruthy(); + } + }); +}); diff --git a/nextjs/lib/content.ts b/nextjs/lib/content.ts new file mode 100644 index 0000000..fcd9a0d --- /dev/null +++ b/nextjs/lib/content.ts @@ -0,0 +1,189 @@ +/** + * Content loading and rendering pipeline. + * Reads posts from the Jekyll _posts directory, parses frontmatter, + * and renders Markdown and Textile to HTML. + */ + +import fs from "fs"; +import path from "path"; +import matter from "gray-matter"; +import { unified } from "unified"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import rehypeRaw from "rehype-raw"; +import rehypeStringify from "rehype-stringify"; +import textile from "textile-js"; + +const POSTS_DIR = path.join(process.cwd(), "..", "jekyll", "_posts", "blog"); +const PAGES_DIR = path.join(process.cwd(), "..", "jekyll"); + +export interface PostFrontmatter { + layout: string; + category: string; + title?: string; + date: string; + updated?: string; + summary?: string; + tags?: string[]; + geo?: { + name?: string; + xy?: string; + }; + canonical?: string; + atomid?: string; + original_service?: string; + original_url?: string; + tumblr_post_type?: string; +} + +export interface Post { + slug: string; + filePath: string; + relativePath: string; + frontmatter: PostFrontmatter; + content: string; + rawContent: string; + format: "markdown" | "textile"; +} + +const markdownProcessor = unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeStringify); + +export async function renderMarkdown(content: string): Promise { + const result = await markdownProcessor.process(content); + return String(result); +} + +export function renderTextile(content: string): string { + return textile(content); +} + +/** + * Parse a post filename to extract date and slug. + * Format: YYYY-MM-DD-slug.{md,textile} + */ +export function parsePostFilename(filename: string): { + date: string; + slug: string; +} | null { + const match = filename.match( + /^(\d{4}-\d{2}-\d{2})-(.+)\.(md|textile)$/ + ); + if (!match) return null; + return { date: match[1], slug: match[2] }; +} + +/** + * Recursively find all post files in the posts directory. + */ +function findPostFiles(dir: string): string[] { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findPostFiles(fullPath)); + } else if (entry.name.match(/\.(md|textile)$/)) { + files.push(fullPath); + } + } + return files; +} + +/** + * Load and parse a single post file. + */ +export function loadPostSync(filePath: string): Post | null { + const filename = path.basename(filePath); + const parsed = parsePostFilename(filename); + if (!parsed) return null; + + const raw = fs.readFileSync(filePath, "utf-8"); + const { data, content } = matter(raw); + const frontmatter = data as PostFrontmatter; + const format = filename.endsWith(".textile") ? "textile" : "markdown"; + const relativePath = path.relative( + path.join(process.cwd(), "..", "jekyll"), + filePath + ); + + return { + slug: parsed.slug, + filePath, + relativePath, + frontmatter, + content: "", // rendered later + rawContent: content, + format, + }; +} + +/** + * Render the content of a post (Markdown or Textile). + */ +export async function renderPost(post: Post): Promise { + const rendered = + post.format === "textile" + ? renderTextile(post.rawContent) + : await renderMarkdown(post.rawContent); + + return { ...post, content: rendered }; +} + +/** + * Load all posts, sorted by date descending. + */ +export function loadAllPostsSync(): Post[] { + const files = findPostFiles(POSTS_DIR); + const posts: Post[] = []; + + for (const file of files) { + const post = loadPostSync(file); + if (post) posts.push(post); + } + + // Sort by date descending + posts.sort((a, b) => { + const dateA = new Date(a.frontmatter.date).getTime(); + const dateB = new Date(b.frontmatter.date).getTime(); + return dateB - dateA; + }); + + return posts; +} + +/** + * Load a page file (about.html, network.md, etc.) from the Jekyll root. + */ +export function loadPage(filename: string): { + frontmatter: Record; + content: string; + format: "markdown" | "html"; +} | null { + const filePath = path.join(PAGES_DIR, filename); + if (!fs.existsSync(filePath)) return null; + + const raw = fs.readFileSync(filePath, "utf-8"); + const { data, content } = matter(raw); + const format = filename.endsWith(".md") ? "markdown" : "html"; + + return { frontmatter: data, content, format }; +} + +/** + * Render a page's content. + */ +export async function renderPageContent( + content: string, + format: "markdown" | "html" +): Promise { + if (format === "markdown") { + return renderMarkdown(content); + } + return content; +} diff --git a/nextjs/lib/dates.test.ts b/nextjs/lib/dates.test.ts new file mode 100644 index 0000000..f5f095b --- /dev/null +++ b/nextjs/lib/dates.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from "vitest"; +import { + parseDate, + formatISO, + formatDisplay, + formatTime, + formatMonthYear, + formatArchivePath, + formatTitleDate, + toUnixTimestamp, + monthName, +} from "./dates"; + +describe("parseDate", () => { + it("parses ISO date with colon in timezone", () => { + const d = parseDate("2022-12-07T00:22:52-08:00"); + expect(d.year).toBe(2022); + expect(d.month).toBe(12); + expect(d.day).toBe(7); + expect(d.hour).toBe(0); + expect(d.minute).toBe(22); + expect(d.second).toBe(52); + expect(d.timezone).toBe("-08:00"); + }); + + it("parses ISO date without colon in timezone", () => { + const d = parseDate("2009-02-15T09:10:19+0000"); + expect(d.timezone).toBe("+00:00"); + expect(d.year).toBe(2009); + }); + + it("throws on invalid date", () => { + expect(() => parseDate("not-a-date")).toThrow("Cannot parse date"); + }); +}); + +describe("formatISO", () => { + it("preserves timezone offset", () => { + expect(formatISO("2022-12-07T00:22:52-08:00")).toBe( + "2022-12-07T00:22:52-08:00" + ); + }); + + it("normalizes timezone without colon", () => { + expect(formatISO("2009-02-15T09:10:19+0000")).toBe( + "2009-02-15T09:10:19+00:00" + ); + }); +}); + +describe("formatDisplay", () => { + it("formats like Jekyll dateformat", () => { + const result = formatDisplay("2022-12-07T00:22:52-08:00"); + expect(result).toBe("Dec 7 2022, 0:22 (-08:00)"); + }); +}); + +describe("formatTime", () => { + it("formats time with timezone", () => { + const result = formatTime("2022-12-07T00:22:52-08:00"); + expect(result).toBe("0:22 (-08:00)"); + }); +}); + +describe("formatMonthYear", () => { + it("formats full month and year", () => { + expect(formatMonthYear("2022-12-07T00:22:52-08:00")).toBe("December 2022"); + }); +}); + +describe("formatArchivePath", () => { + it("formats as YYYY/MM", () => { + expect(formatArchivePath("2022-12-07T00:22:52-08:00")).toBe("2022/12"); + expect(formatArchivePath("2009-02-15T09:10:19+0000")).toBe("2009/02"); + }); +}); + +describe("formatTitleDate", () => { + it("formats for auto-generated titles", () => { + expect(formatTitleDate("2022-12-07T00:22:52-08:00")).toBe( + "December 7, 2022" + ); + }); +}); + +describe("toUnixTimestamp", () => { + it("converts to Unix timestamp", () => { + const ts = toUnixTimestamp("2022-12-07T00:22:52-08:00"); + // 2022-12-07T00:22:52-08:00 = 2022-12-07T08:22:52Z + expect(ts).toBe(1670401372); + }); +}); + +describe("monthName", () => { + it("returns month name from number", () => { + expect(monthName(1)).toBe("January"); + expect(monthName(12)).toBe("December"); + }); +}); diff --git a/nextjs/lib/dates.ts b/nextjs/lib/dates.ts new file mode 100644 index 0000000..a7e3a33 --- /dev/null +++ b/nextjs/lib/dates.ts @@ -0,0 +1,143 @@ +/** + * Timezone-preserving date formatting utilities. + * Ports the Jekyll liquid_standard_filters.rb and jekyll_utils.rb plugins. + * + * Key principle: dates are kept as ISO 8601 strings to preserve timezone offsets. + * We never convert to JS Date objects for display purposes, as that would lose + * the original timezone. + */ + +const MONTHS_FULL = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +]; +const MONTHS_SHORT = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +export interface ParsedDate { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + timezone: string; +} + +/** + * Parse an ISO 8601 date string preserving the timezone offset. + */ +export function parseDate(dateStr: string): ParsedDate { + // Handle formats like: 2022-12-07T00:22:52-08:00, 2009-02-15T09:10:19+0000 + const match = dateStr.match( + /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([-+]\d{2}:?\d{2})$/ + ); + + if (!match) { + throw new Error(`Cannot parse date: ${dateStr}`); + } + + let tz = match[7]; + // Normalize +0000 to +00:00 + if (tz.length === 5) { + tz = tz.slice(0, 3) + ":" + tz.slice(3); + } + + return { + year: parseInt(match[1], 10), + month: parseInt(match[2], 10), + day: parseInt(match[3], 10), + hour: parseInt(match[4], 10), + minute: parseInt(match[5], 10), + second: parseInt(match[6], 10), + timezone: tz, + }; +} + +/** + * Format a date string in ISO 8601 format, preserving timezone. + * Equivalent to strftime("%FT%T%:z") + */ +export function formatISO(dateStr: string): string { + const d = parseDate(dateStr); + const pad = (n: number) => n.toString().padStart(2, "0"); + return `${d.year}-${pad(d.month)}-${pad(d.day)}T${pad(d.hour)}:${pad(d.minute)}:${pad(d.second)}${d.timezone}`; +} + +/** + * Format for display: "Dec 7 2022, 0:22 (-08:00)" + * Equivalent to strftime("%b %e %Y, %k:%M (%z)") + */ +export function formatDisplay(dateStr: string): string { + const d = parseDate(dateStr); + const month = MONTHS_SHORT[d.month - 1]; + const day = d.day.toString().padStart(2, " "); + const minute = d.minute.toString().padStart(2, "0"); + return `${month} ${day} ${d.year}, ${d.hour}:${minute} (${d.timezone})`; +} + +/** + * Format time only: "0:22 (-08:00)" + * Equivalent to strftime("%k:%M (%z)") + */ +export function formatTime(dateStr: string): string { + const d = parseDate(dateStr); + const minute = d.minute.toString().padStart(2, "0"); + return `${d.hour}:${minute} (${d.timezone})`; +} + +/** + * Format as full month and year: "December 2022" + */ +export function formatMonthYear(dateStr: string): string { + const d = parseDate(dateStr); + return `${MONTHS_FULL[d.month - 1]} ${d.year}`; +} + +/** + * Format as full month name: "December" + */ +export function formatMonth(dateStr: string): string { + const d = parseDate(dateStr); + return MONTHS_FULL[d.month - 1]; +} + +/** + * Get the full month name from a month number (1-12). + */ +export function monthName(month: number): string { + return MONTHS_FULL[month - 1]; +} + +/** + * Format as "YYYY/MM" for archive URLs. + */ +export function formatArchivePath(dateStr: string): string { + const d = parseDate(dateStr); + return `${d.year}/${d.month.toString().padStart(2, "0")}`; +} + +/** + * Format for Atom feed: "January 7, 2022" + * Used for auto-generated titles on Tumblr imports. + */ +export function formatTitleDate(dateStr: string): string { + const d = parseDate(dateStr); + return `${MONTHS_FULL[d.month - 1]} ${d.day}, ${d.year}`; +} + +/** + * Get Unix timestamp from a date string. + */ +export function toUnixTimestamp(dateStr: string): number { + return Math.floor(new Date(dateStr).getTime() / 1000); +} + +/** + * Get XML schema date format for Atom feeds. + */ +export function toXmlSchema(date: Date): string { + return date.toISOString(); +} diff --git a/nextjs/lib/global.css b/nextjs/lib/global.css new file mode 100644 index 0000000..7e8a227 --- /dev/null +++ b/nextjs/lib/global.css @@ -0,0 +1,195 @@ +/* benward.uk — Global styles */ + +:root { + --BenWard-canvasColor: #253820; + --BenWard-pageColor: #F7F7FF; + --BenWard-bodyTextColor: #2B2D42; + --BenWard-linkColor: #2B6B9E; + --BenWard-accentColor: #B22D43; + --BenWard-asideBackground: #2B2D42; + --BenWard-asideTextColor: #F7F7FF; + --BenWard-logoColor: #F7F7FF; + --BenWard-footerTextColor: #c4d9be; + --BenWard-footerLinkColor: #F7F7FF; +} + +/* Font Sets */ +html, +h1, h2, h3, +article .dateline, +ul.tags { + font: 20px/28px sans-serif; +} + +article { + font-family: serif; + font-weight: 200; +} + +h1, h2, h3 { + font-weight: bold; +} + +/* Site */ +html { + margin: 0 auto; + max-width: 38em; + background: var(--BenWard-canvasColor); + background-image: + repeating-linear-gradient(-65deg, rgba(0, 0, 0,0) 1px, rgba(0, 0, 0, .2) 3px, rgba(0, 0, 0,0) 5px), + repeating-linear-gradient(65deg, rgba(0, 0, 0 ,0) 0, rgba(0, 0, 0, .2) 3px, rgba(0, 0, 0, 0) 5px), + linear-gradient(20deg, rgba(0, 0, 0, .5) 0, rgba(0, 0, 0, 0) 100%); + color: var(--BenWard-bodyTextColor); +} + +body { margin: 0 16px; } + +h1 { + font-size: 130%; + line-height: 150%; + margin: 0 0 16px 0; +} +h2 { + font-size: 110%; + line-height: 150%; + margin: 0 0 4px 0; +} +h3, h4 { + font-size: 100%; + line-height: 150%; +} +h3 { margin: 0 0 4px 0; } + +a:link { + color: var(--BenWard-linkColor); + text-decoration: none; +} +a:visited { color: var(--BenWard-linkColor); } +a:hover, a:focus { text-decoration: underline; } +a:active { text-decoration: underline; } + +q { quotes: none; } + +ol, ul, li { + margin: 0; + padding: 0; + list-style: none; +} + +article li { + margin: 2px 0 2px 20px; + list-style-position: outside; +} +article ul li { list-style-type: disc; } +article ol li { list-style-type: decimal; } + +blockquote { + margin: 6px 12px 6px 0; + padding-left: 12px; + border: 4px solid var(--BenWard-accentColor); + border-width: 0 0 0 4px; +} + +article aside { + margin: 32px 0 32px -32px; + font-size: 90%; + background-color: var(--BenWard-asideBackground); + color: var(--BenWard-asideTextColor); + padding: 16px; +} + +p, dl, ol, ul { + margin: 0 0 16px; +} + +kbd, output, samp, code { + font-family: monospace; + display: inline-block; + font-size: 70%; + padding: 2px; + color: var(--BenWard-accentColor); + border-radius: 3px; +} + +pre code { + display: block; + padding: 4px; + max-width: 100%; + overflow-x: scroll; +} + +article img { + text-align: center; + max-width: 100%; + margin: 0 -32px; +} + +body > article > .e-content > blockquote:first-child { + border: 0; + font-size: 200%; + line-height: 130%; + padding: 0; + margin: 16px 0; +} + +article .full-bleed { margin: 0 -32px; } +article .full-bleed img { width: 100%; margin: 0; } +article .top-full-bleed { margin-top: -32px; } +article .pull-left { float: left; margin: 0 8px 0 -32px; } +article .pull-right { float: right; margin: 0 -32px 0 8px; } + +/* Button */ +.button:link, +.button:active, +.button:visited, +.button:hover, +.button:focus { + display: inline-block; + background-color: #f8f8f8; + background-image: linear-gradient( #fff, #dedede); + border: #ccc solid 1px; + border-radius: 3px; + padding: 3px 8px; + color: var(--BenWard-bodyTextColor); + font: bold 13px/20px Helvetica, sans-serif; + text-shadow: 0 1px 0 rgba(255,255,255,.5); + user-select: none; + cursor: pointer; + height: 18px; +} +.button:hover, +.button:focus { + border-color: var(--BenWard-linkColor); +} + +/* Legacy comments */ +#comments h2 { font-size: 100%; } +#comments .about-comments { font-size: 80%; } +#comments > ol { margin: 0; padding: 0; } +#comments > ol > li { margin: 0 0 16px 0; list-style: none; } +#comments .hentry { position: relative; padding-left: 30px; } +#comments .hentry .photo { position: absolute; top: 0; left: 0; width: 50px; height: 50px; } + +/* Narrow */ +@media (max-width: 420px) { + body { margin: 0; } + header, footer, body > article, body > section { padding: 16px; } + header { padding: 32px 16px; } + .cover { margin: -16px -16px 16px -16px; } + .cover p { padding: 8px 16px; } + article .full-bleed { margin: 0 -16px; } + article .pull-left { margin: 0 8px 0 -16px; } + article .pull-right { margin: 0 -16px 0 8px; } + section.h-feed header, + article header, + section.h-feed footer, + article footer { + margin: 16px -16px -16px -16px; + padding: 0 16px; + } + section.h-feed header, + article header { margin: -16px -16px 16px -16px; } + section.h-feed nav a, + article.h-entry nav a { font-size: 80%; } + article > aside { margin-left: -8px; } +} diff --git a/nextjs/lib/humans-comparison.test.ts b/nextjs/lib/humans-comparison.test.ts new file mode 100644 index 0000000..8ac7997 --- /dev/null +++ b/nextjs/lib/humans-comparison.test.ts @@ -0,0 +1,108 @@ +/** + * Verify humans.txt content matches Jekyll template expectations. + * + * We test the content generation directly since the Web API `Response` + * class isn't available in the jsdom test environment. + */ + +import { describe, it, expect, beforeAll } from "vitest"; +import { toXmlSchema } from "@/lib/dates"; + +/** Reproduce the exact content from the route handler */ +function generateHumansTxt(): string { + return `/* TEAM */ + Protagonist: Ben Ward + URL: https://benward.uk/about + Twitter: @benward + From: Cambridge, United Kingdom + +/* SITE */ + Last update: ${toXmlSchema(new Date())} + Language: English (British) + Doctype: HTML5 + IDE: Nova, Visual Studio Code, Sublime Text, iA Writer +`; +} + +/** + * Jekyll template for reference: + * + * /* TEAM *\/ + * Protagonist: Ben Ward + * URL: https://benward.uk/about + * Twitter: @benward + * From: Cambridge, United Kingdom + * + * /* SITE *\/ + * Last update: {{site.time | date_to_xmlschema}} + * Language: English (British) + * Doctype: HTML5 + * IDE: Nova, Visual Studio Code, Sublime Text, iA Writer + */ + +describe("humans.txt content", () => { + let body: string; + + beforeAll(() => { + body = generateHumansTxt(); + }); + + it("contains TEAM section", () => { + expect(body).toContain("/* TEAM */"); + }); + + it("has correct protagonist", () => { + expect(body).toContain("Protagonist: Ben Ward"); + }); + + it("has correct URL", () => { + expect(body).toContain("URL: https://benward.uk/about"); + }); + + it("has Twitter handle", () => { + expect(body).toContain("Twitter: @benward"); + }); + + it("has location", () => { + expect(body).toContain("From: Cambridge, United Kingdom"); + }); + + it("contains SITE section", () => { + expect(body).toContain("/* SITE */"); + }); + + it("has Last update as ISO 8601 date (matches Jekyll date_to_xmlschema)", () => { + // Jekyll's date_to_xmlschema outputs ISO 8601 format + expect(body).toMatch(/Last update: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + it("has correct language", () => { + expect(body).toContain("Language: English (British)"); + }); + + it("has correct doctype", () => { + expect(body).toContain("Doctype: HTML5"); + }); + + it("has IDE list matching Jekyll template", () => { + expect(body).toContain( + "IDE: Nova, Visual Studio Code, Sublime Text, iA Writer" + ); + }); + + it("matches Jekyll template structure exactly", () => { + // Verify the overall structure mirrors the Jekyll template line-by-line + const lines = body.split("\n").map((l) => l.trimEnd()); + expect(lines[0]).toBe("/* TEAM */"); + expect(lines[1]).toBe(" Protagonist: Ben Ward"); + expect(lines[2]).toBe(" URL: https://benward.uk/about"); + expect(lines[3]).toBe(" Twitter: @benward"); + expect(lines[4]).toBe(" From: Cambridge, United Kingdom"); + expect(lines[5]).toBe(""); + expect(lines[6]).toBe("/* SITE */"); + expect(lines[7]).toMatch(/^ {2}Last update: \d{4}-\d{2}-\d{2}T/); + expect(lines[8]).toBe(" Language: English (British)"); + expect(lines[9]).toBe(" Doctype: HTML5"); + expect(lines[10]).toBe(" IDE: Nova, Visual Studio Code, Sublime Text, iA Writer"); + }); +}); diff --git a/nextjs/lib/posts.test.ts b/nextjs/lib/posts.test.ts new file mode 100644 index 0000000..02a992c --- /dev/null +++ b/nextjs/lib/posts.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from "vitest"; +import { cleanUrl, githubSourceUrl, generateTitle, getExcerpt, enrichPost } from "./posts"; +import type { Post } from "./content"; + +function makePost(overrides: Partial = {}): Post { + return { + slug: "test-post", + filePath: "/path/to/post.md", + relativePath: "_posts/blog/2022/2022-12-07-test-post.md", + frontmatter: { + layout: "blog", + category: "blog", + title: "Test Post", + date: "2022-12-07T00:22:52-08:00", + tags: ["test"], + }, + content: "", + rawContent: "# Test", + format: "markdown", + ...overrides, + }; +} + +describe("cleanUrl", () => { + it("generates clean URL from category and slug", () => { + const post = makePost(); + expect(cleanUrl(post)).toBe("/blog/test-post"); + }); + + it("defaults to /blog/ when no category", () => { + const post = makePost({ + frontmatter: { ...makePost().frontmatter, category: "" }, + }); + // Falls back to "blog" default in cleanUrl + expect(cleanUrl(post)).toBe("/blog/test-post"); + }); +}); + +describe("githubSourceUrl", () => { + it("generates correct GitHub URL", () => { + const post = makePost(); + const url = githubSourceUrl(post); + expect(url).toBe( + "https://github.com/BenWard/benward-web/tree/main/jekyll/_posts/blog/2022/2022-12-07-test-post.md" + ); + }); +}); + +describe("generateTitle", () => { + it("generates title from date", () => { + expect(generateTitle("2022-12-07T00:22:52-08:00")).toBe( + "December 7, 2022" + ); + }); + + it("returns 'Post' for invalid date", () => { + expect(generateTitle("invalid")).toBe("Post"); + }); +}); + +describe("getExcerpt", () => { + it("uses summary frontmatter if present", () => { + const post = makePost({ + frontmatter: { + ...makePost().frontmatter, + summary: "My summary", + }, + }); + expect(getExcerpt(post)).toBe("My summary"); + }); + + it("returns empty string when no summary", () => { + const post = makePost(); + expect(getExcerpt(post)).toBe(""); + }); +}); + +describe("enrichPost", () => { + it("adds all computed properties", () => { + const post = makePost(); + const enriched = enrichPost(post); + expect(enriched.cleanUrl).toBe("/blog/test-post"); + expect(enriched.githubSourceUrl).toContain("github.com"); + expect(enriched.globalDate).toBe("2022-12-07T00:22:52-08:00"); + expect(enriched.dateTitle).toBe(false); + }); + + it("auto-generates title for untitled posts", () => { + const post = makePost({ + frontmatter: { + ...makePost().frontmatter, + title: undefined, + }, + }); + const enriched = enrichPost(post); + expect(enriched.frontmatter.title).toBe("December 7, 2022"); + expect(enriched.dateTitle).toBe(true); + }); +}); diff --git a/nextjs/lib/posts.ts b/nextjs/lib/posts.ts new file mode 100644 index 0000000..6bf4fe6 --- /dev/null +++ b/nextjs/lib/posts.ts @@ -0,0 +1,73 @@ +/** + * Post enrichment utilities. + * Port of the Jekyll jekyl_post.rb plugin. + */ + +import { siteConfig } from "@/config/site"; +import type { Post } from "./content"; +import { formatTitleDate } from "./dates"; + +export interface EnrichedPost extends Post { + cleanUrl: string; + githubSourceUrl: string; + globalDate: string; + dateTitle: boolean; + excerpt: string; +} + +/** + * Generate a clean URL (without .html) for a post. + * Jekyll permalink: /:categories/:title.html -> /blog/slug + */ +export function cleanUrl(post: Post): string { + const category = post.frontmatter.category || "blog"; + return `/${category}/${post.slug}`; +} + +/** + * Generate a GitHub source URL for a post. + */ +export function githubSourceUrl(post: Post): string { + return `https://github.com/${siteConfig.githubSlug}/tree/main/${siteConfig.gitBase}/${post.relativePath}`; +} + +/** + * Auto-generate a title from the date for untitled posts (Tumblr imports). + */ +export function generateTitle(dateStr: string): string { + try { + return formatTitleDate(dateStr); + } catch { + return "Post"; + } +} + +/** + * Get the excerpt for a post: prefer `summary` frontmatter, otherwise empty. + */ +export function getExcerpt(post: Post): string { + return post.frontmatter.summary || ""; +} + +/** + * Enrich a post with computed properties. + */ +export function enrichPost(post: Post): EnrichedPost { + const hasTitle = !!post.frontmatter.title; + const title = hasTitle + ? post.frontmatter.title! + : generateTitle(post.frontmatter.date); + + return { + ...post, + frontmatter: { + ...post.frontmatter, + title, + }, + cleanUrl: cleanUrl(post), + githubSourceUrl: githubSourceUrl(post), + globalDate: post.frontmatter.date, + dateTitle: !hasTitle, + excerpt: getExcerpt(post), + }; +} diff --git a/nextjs/lib/rendering-comparison.test.ts b/nextjs/lib/rendering-comparison.test.ts new file mode 100644 index 0000000..7eb8dde --- /dev/null +++ b/nextjs/lib/rendering-comparison.test.ts @@ -0,0 +1,260 @@ +/** + * Rendering comparison tests: verify Next.js output matches Jekyll expectations + * for specific blog posts. + */ + +import { describe, it, expect, beforeAll } from "vitest"; +import path from "path"; +import { loadPostSync, renderPost } from "./content"; +import { enrichPost } from "./posts"; +import { formatDisplay, formatISO, formatArchivePath, formatMonthYear } from "./dates"; +import { shortlink } from "./base60"; +import { generateTagId } from "./tag-id"; +import { siteConfig } from "@/config/site"; + +const JEKYLL_BASE = path.join(process.cwd(), "..", "jekyll"); + +describe("Markdown post rendering: top-5-2017", () => { + const filePath = path.join( + JEKYLL_BASE, + "_posts", + "blog", + "2018", + "2018-01-02-top-5-2017.md" + ); + + let post: ReturnType; + let renderedContent: string; + + beforeAll(async () => { + const raw = loadPostSync(filePath)!; + post = enrichPost(raw); + const rendered = await renderPost(post); + renderedContent = rendered.content; + }); + + describe("frontmatter", () => { + it("has correct title", () => { + expect(post.frontmatter.title).toBe("Parboiled Kettle: Ben's 5 for 2017"); + }); + + it("has correct category", () => { + expect(post.frontmatter.category).toBe("blog"); + }); + + it("has correct date", () => { + expect(post.frontmatter.date).toBe("2018-01-02T12:00:00-08:00"); + }); + + it("has canonical URL", () => { + expect(post.frontmatter.canonical).toBe( + "https://bff.fm/posts/by/benward/1904" + ); + }); + + it("has geo data", () => { + expect(post.frontmatter.geo?.name).toBe( + "San Francisco, United States" + ); + expect(post.frontmatter.geo?.xy).toBe("37.751,-122.436"); + }); + + it("has tags", () => { + expect(post.frontmatter.tags).toEqual(["music", "lists", "bffdotfm"]); + }); + + it("has summary/excerpt", () => { + expect(post.excerpt).toContain("In 2017, Simon, myself"); + }); + }); + + describe("enriched properties", () => { + it("generates clean URL", () => { + // Jekyll permalink: /:categories/:title.html -> /blog/top-5-2017 + expect(post.cleanUrl).toBe("/blog/top-5-2017"); + }); + + it("generates GitHub source URL", () => { + expect(post.githubSourceUrl).toBe( + "https://github.com/BenWard/benward-web/tree/main/jekyll/_posts/blog/2018/2018-01-02-top-5-2017.md" + ); + }); + + it("preserves global date with timezone", () => { + expect(post.globalDate).toBe("2018-01-02T12:00:00-08:00"); + }); + + it("is not a date-titled post", () => { + expect(post.dateTitle).toBe(false); + }); + }); + + describe("date formatting (matching Jekyll strftime)", () => { + it("formatDisplay matches Jekyll dateformat", () => { + // Jekyll: "%b %e %Y, %k:%M (%z)" -> "Jan 2 2018, 12:00 (-08:00)" + expect(formatDisplay(post.globalDate)).toBe("Jan 2 2018, 12:00 (-08:00)"); + }); + + it("formatISO matches Jekyll isodateformat", () => { + // Jekyll: "%FT%T%:z" -> "2018-01-02T12:00:00-08:00" + expect(formatISO(post.globalDate)).toBe("2018-01-02T12:00:00-08:00"); + }); + + it("archivePath matches Jekyll date: '%Y/%m'", () => { + expect(formatArchivePath(post.globalDate)).toBe("2018/01"); + }); + + it("monthYear matches Jekyll date: '%B %Y'", () => { + expect(formatMonthYear(post.globalDate)).toBe("January 2018"); + }); + }); + + describe("shortlink", () => { + it("generates base60-encoded shortlink", () => { + const short = shortlink(post.globalDate, siteConfig.shortdomain); + expect(short).toMatch(/^https:\/\/bnwrd\.me\/.+$/); + }); + }); + + describe("tag ID", () => { + it("generates tag URI with benward.uk (post is 2018+)", () => { + const tagId = generateTagId(post.globalDate, post.cleanUrl); + expect(tagId).toBe("tag:benward.uk,2018-01-02:/blog/top-5-2017"); + }); + }); + + describe("rendered content", () => { + it("renders non-empty content", () => { + expect(renderedContent.length).toBeGreaterThan(0); + }); + + it("renders paragraphs", () => { + expect(renderedContent).toContain("

"); + }); + + it("preserves raw HTML iframes", () => { + expect(renderedContent).toContain(" { + expect(renderedContent).toContain(" { + expect(renderedContent).toContain("Kelly Lee Owens"); + }); + + it("renders italic text", () => { + expect(renderedContent).toContain(""); + }); + + it("preserves inline HTML links", () => { + expect(renderedContent).toContain('href="http://bff.fm/shows/eclectic-kettle"'); + }); + }); +}); + +describe("Textile post rendering: simple_microformats", () => { + const filePath = path.join( + JEKYLL_BASE, + "_posts", + "blog", + "2005", + "2005-06-29-simple_microformats.textile" + ); + + let post: ReturnType; + let renderedContent: string; + + beforeAll(async () => { + const raw = loadPostSync(filePath)!; + post = enrichPost(raw); + const rendered = await renderPost(post); + renderedContent = rendered.content; + }); + + describe("frontmatter", () => { + it("has correct title", () => { + expect(post.frontmatter.title).toBe("First thoughts on Microformats"); + }); + + it("has correct date with timezone", () => { + expect(post.frontmatter.date).toBe("2005-06-29T00:02:17+01:00"); + }); + + it("has tags", () => { + expect(post.frontmatter.tags).toEqual(["all", "technology", "web_standards"]); + }); + + it("has atomid", () => { + expect(post.frontmatter.atomid).toBe( + "tag:benward.me,2005-06-29:/blog/simple_microformats" + ); + }); + }); + + describe("enriched properties", () => { + it("generates clean URL", () => { + expect(post.cleanUrl).toBe("/blog/simple_microformats"); + }); + + it("generates GitHub source URL", () => { + expect(post.githubSourceUrl).toContain( + "2005-06-29-simple_microformats.textile" + ); + }); + + it("preserves timezone", () => { + expect(post.globalDate).toBe("2005-06-29T00:02:17+01:00"); + }); + }); + + describe("date formatting (matching Jekyll strftime)", () => { + it("formatDisplay matches Jekyll dateformat", () => { + // Jekyll: "%b %e %Y, %k:%M (%z)" -> "Jun 29 2005, 0:02 (+01:00)" + expect(formatDisplay(post.globalDate)).toBe("Jun 29 2005, 0:02 (+01:00)"); + }); + + it("formatISO preserves timezone", () => { + expect(formatISO(post.globalDate)).toBe("2005-06-29T00:02:17+01:00"); + }); + + it("archivePath is correct", () => { + expect(formatArchivePath(post.globalDate)).toBe("2005/06"); + }); + }); + + describe("tag ID", () => { + it("uses atomid from frontmatter (not generated)", () => { + // This post has an explicit atomid - the feed should use it + expect(post.frontmatter.atomid).toBe( + "tag:benward.me,2005-06-29:/blog/simple_microformats" + ); + }); + + it("would generate benward.me domain (pre-2018)", () => { + const tagId = generateTagId(post.globalDate, post.cleanUrl); + expect(tagId).toContain("benward.me"); + }); + }); + + describe("rendered content (textile)", () => { + it("renders non-empty content", () => { + expect(renderedContent.length).toBeGreaterThan(0); + }); + + it("preserves HTML links from textile source", () => { + expect(renderedContent).toContain("microformats.org"); + }); + + it("preserves blockquote from textile source", () => { + expect(renderedContent).toContain(" { + expect(renderedContent).toContain(" { + it("converts simple years", () => { + expect(romanize(2024)).toBe("MMXXIV"); + expect(romanize(2000)).toBe("MM"); + expect(romanize(1999)).toBe("MCMXCIX"); + }); + + it("handles single-digit values", () => { + expect(romanize(1)).toBe("I"); + expect(romanize(4)).toBe("IV"); + expect(romanize(9)).toBe("IX"); + }); + + it("handles blog-relevant years", () => { + expect(romanize(2005)).toBe("MMV"); + expect(romanize(2009)).toBe("MMIX"); + expect(romanize(2022)).toBe("MMXXII"); + }); + + it("returns empty string for 0", () => { + expect(romanize(0)).toBe(""); + }); +}); diff --git a/nextjs/lib/romans.ts b/nextjs/lib/romans.ts new file mode 100644 index 0000000..152e17e --- /dev/null +++ b/nextjs/lib/romans.ts @@ -0,0 +1,34 @@ +/** + * Convert an integer to Roman numerals. + * Port of the Jekyll romans.rb plugin. + */ + +const NUMERALS: [string, number][] = [ + ["M", 1000], + ["CM", 900], + ["D", 500], + ["CD", 400], + ["C", 100], + ["XC", 90], + ["L", 50], + ["XL", 40], + ["X", 10], + ["IX", 9], + ["V", 5], + ["IV", 4], + ["I", 1], +]; + +export function romanize(year: number): string { + let remaining = Math.floor(year); + let result = ""; + + for (const [numeral, value] of NUMERALS) { + while (remaining >= value) { + result += numeral; + remaining -= value; + } + } + + return result; +} diff --git a/nextjs/lib/shortlinks.test.ts b/nextjs/lib/shortlinks.test.ts new file mode 100644 index 0000000..87168f6 --- /dev/null +++ b/nextjs/lib/shortlinks.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { decodeBase60, buildShortlinkMap, resolveShortlink } from "./shortlinks"; +import { encodeBase60 } from "./base60"; + +describe("decodeBase60", () => { + it("decodes 0", () => { + expect(decodeBase60("0")).toBe(0); + }); + + it("round-trips with encodeBase60", () => { + const timestamps = [1670401372, 1514923200, 1120000937]; + for (const ts of timestamps) { + const encoded = encodeBase60(ts); + expect(decodeBase60(encoded)).toBe(ts); + } + }); + + it("returns -1 for invalid characters", () => { + expect(decodeBase60("!!!")).toBe(-1); + }); +}); + +describe("buildShortlinkMap", () => { + it("returns a non-empty map", () => { + const map = buildShortlinkMap(); + expect(map.size).toBeGreaterThan(0); + }); + + it("maps base60 IDs to URLs", () => { + const map = buildShortlinkMap(); + for (const [id, url] of map) { + expect(id.length).toBeGreaterThan(0); + expect(url).toMatch(/^\//); + break; // just check first entry + } + }); +}); + +describe("resolveShortlink", () => { + it("resolves a known post", () => { + // Build the map to find a valid ID + const map = buildShortlinkMap(); + const [firstId, firstUrl] = map.entries().next().value!; + expect(resolveShortlink(firstId)).toBe(firstUrl); + }); + + it("returns null for unknown ID", () => { + expect(resolveShortlink("ZZZZZZZZZZ")).toBeNull(); + }); +}); diff --git a/nextjs/lib/shortlinks.ts b/nextjs/lib/shortlinks.ts new file mode 100644 index 0000000..60ec6f8 --- /dev/null +++ b/nextjs/lib/shortlinks.ts @@ -0,0 +1,48 @@ +/** + * Shortlink resolution: maps base60-encoded IDs back to post URLs. + * + * The shortlink ID is a base60 encoding of the post's Unix timestamp. + * To resolve, we decode the ID to a timestamp and find the matching post. + */ + +import { encodeBase60 } from "./base60"; +import { loadAllPostsSync } from "./content"; +import { enrichPost, type EnrichedPost } from "./posts"; + +const CHARSET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"; + +export function decodeBase60(s: string): number { + let result = 0; + for (const char of s) { + const val = CHARSET.indexOf(char); + if (val === -1) return -1; + result = result * 60 + val; + } + return result; +} + +/** + * Build a lookup map from base60 ID to post URL. + */ +export function buildShortlinkMap(): Map { + const posts = loadAllPostsSync().map(enrichPost); + const map = new Map(); + + for (const post of posts) { + const timestamp = Math.floor(new Date(post.frontmatter.date).getTime() / 1000); + const id = encodeBase60(timestamp); + const url = post.frontmatter.canonical || post.cleanUrl; + map.set(id, url); + } + + return map; +} + +/** + * Resolve a shortlink ID to a destination URL. + * Returns the URL or null if not found. + */ +export function resolveShortlink(id: string): string | null { + const map = buildShortlinkMap(); + return map.get(id) || null; +} diff --git a/nextjs/lib/tag-id.test.ts b/nextjs/lib/tag-id.test.ts new file mode 100644 index 0000000..95b5ce5 --- /dev/null +++ b/nextjs/lib/tag-id.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { generateTagId } from "./tag-id"; + +describe("generateTagId", () => { + it("uses benward.uk for posts from 2018+", () => { + const result = generateTagId( + "2022-12-07T00:22:52-08:00", + "/blog/some-post" + ); + expect(result).toBe("tag:benward.uk,2022-12-07:/blog/some-post"); + }); + + it("uses benward.me for posts before 2018", () => { + const result = generateTagId( + "2009-02-15T09:10:19+0000", + "/blog/old-post" + ); + expect(result).toBe("tag:benward.me,2009-02-15:/blog/old-post"); + }); + + it("includes the clean URL path", () => { + const result = generateTagId( + "2020-01-01T00:00:00+00:00", + "/blog/hello-world" + ); + expect(result).toContain("/blog/hello-world"); + }); +}); diff --git a/nextjs/lib/tag-id.ts b/nextjs/lib/tag-id.ts new file mode 100644 index 0000000..9a0d51a --- /dev/null +++ b/nextjs/lib/tag-id.ts @@ -0,0 +1,18 @@ +/** + * Generate RFC 4151 tag URIs for Atom feed entries. + * Port of the Jekyll tag_id.rb plugin. + */ + +import { siteConfig } from "@/config/site"; + +export function generateTagId(dateStr: string, cleanUrl: string): string { + const date = new Date(dateStr); + const year = date.getUTCFullYear(); + + const domain = + year >= 2018 ? siteConfig.currentDomain : siteConfig.legacyDomain; + + const dateFormatted = dateStr.slice(0, 10); // YYYY-MM-DD + + return `tag:${domain},${dateFormatted}:${cleanUrl}`; +} diff --git a/nextjs/next-env.d.ts b/nextjs/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/nextjs/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/nextjs/next.config.ts b/nextjs/next.config.ts new file mode 100644 index 0000000..86d5965 --- /dev/null +++ b/nextjs/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + async redirects() { + return [ + { + source: "/blog/:slug.html", + destination: "/blog/:slug", + permanent: true, + }, + ]; + }, +}; + +export default nextConfig; diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json new file mode 100644 index 0000000..aa28c97 --- /dev/null +++ b/nextjs/package-lock.json @@ -0,0 +1,4211 @@ +{ + "name": "benward-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "benward-web", + "version": "1.0.0", + "dependencies": { + "gray-matter": "^4", + "next": "^16", + "react": "^19", + "react-dom": "^19", + "rehype-raw": "^7", + "rehype-stringify": "^10", + "remark-parse": "^11", + "remark-rehype": "^11", + "textile-js": "^2", + "unified": "^11" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^29.0.1", + "npm-check-updates": "^19.6.6", + "typescript": "^6", + "vitest": "^4.1.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@next/env": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", + "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", + "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", + "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", + "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", + "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", + "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", + "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", + "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", + "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", + "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.1", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.1", + "@next/swc-darwin-x64": "16.2.1", + "@next/swc-linux-arm64-gnu": "16.2.1", + "@next/swc-linux-arm64-musl": "16.2.1", + "@next/swc-linux-x64-gnu": "16.2.1", + "@next/swc-linux-x64-musl": "16.2.1", + "@next/swc-win32-arm64-msvc": "16.2.1", + "@next/swc-win32-x64-msvc": "16.2.1", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/npm-check-updates": { + "version": "19.6.6", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.6.6.tgz", + "integrity": "sha512-AvlRcnlUEyBEJfblUSjYMJwYKvCIWDRuCDa6x3hyUMTMkI3kslmFm0LDqwgzQfshfNh0Z3ouKiA4fLjRN7HejQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "ncu": "build/cli.js", + "npm-check-updates": "build/cli.js" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=8.12.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textile-js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/textile-js/-/textile-js-2.1.1.tgz", + "integrity": "sha512-6yP8bPtL364lb/Pu9IQh/pu9aFXQN2VSUQrzVtOrjQs0pSc+jX0iczNPcMn5+MNvmoP3piCPxFb0hPBS7DmfFg==", + "license": "MIT", + "bin": { + "textile-js": "bin/textile" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/nextjs/package.json b/nextjs/package.json new file mode 100644 index 0000000..b196cd2 --- /dev/null +++ b/nextjs/package.json @@ -0,0 +1,39 @@ +{ + "name": "benward-web", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "test": "vitest run", + "test:watch": "vitest", + "packages:sync": "npm install", + "packages:upgrade": "npm-check-updates -u --target minor --dep prod,dev,peer,optional && npm run packages:sync && npm run packages:upgrade:check && echo \"MINOR upgrades successfully applied to package.json\"", + "packages:upgrade:check": "echo \"Checking for available upgrades.\" && npm-check-updates --target minor --dep prod,dev,peer,optional --format group && echo && echo \"Checking for available MAJOR version updates (may including breaking changes.)\" && echo && npm-check-updates --target latest --dep prod,dev,peer,optional --format group | awk '/Major/ {flag=1} flag' && echo \"MAJOR upgrades must be applied manually by editing package.json. Test carefully!\"" + }, + "dependencies": { + "gray-matter": "^4", + "next": "^16", + "react": "^19", + "react-dom": "^19", + "rehype-raw": "^7", + "rehype-stringify": "^10", + "remark-parse": "^11", + "remark-rehype": "^11", + "textile-js": "^2", + "unified": "^11" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^29.0.1", + "npm-check-updates": "^19.6.6", + "typescript": "^6", + "vitest": "^4.1.2" + } +} diff --git a/nextjs/public/images/damn.jpg b/nextjs/public/images/damn.jpg new file mode 100644 index 0000000..38c7867 Binary files /dev/null and b/nextjs/public/images/damn.jpg differ diff --git a/nextjs/public/images/records.jpg b/nextjs/public/images/records.jpg new file mode 100644 index 0000000..998baf0 Binary files /dev/null and b/nextjs/public/images/records.jpg differ diff --git a/nextjs/public/static/identity/BenWard.jpg b/nextjs/public/static/identity/BenWard.jpg new file mode 100755 index 0000000..fd1cdfc Binary files /dev/null and b/nextjs/public/static/identity/BenWard.jpg differ diff --git a/nextjs/public/static/identity/BenWard.vcf b/nextjs/public/static/identity/BenWard.vcf new file mode 100755 index 0000000..89fa0db Binary files /dev/null and b/nextjs/public/static/identity/BenWard.vcf differ diff --git a/nextjs/public/static/identity/BenWardAvatar.jpg b/nextjs/public/static/identity/BenWardAvatar.jpg new file mode 100755 index 0000000..094e600 Binary files /dev/null and b/nextjs/public/static/identity/BenWardAvatar.jpg differ diff --git a/nextjs/public/static/identity/BenWardAvatar32.jpg b/nextjs/public/static/identity/BenWardAvatar32.jpg new file mode 100755 index 0000000..30eaeaa Binary files /dev/null and b/nextjs/public/static/identity/BenWardAvatar32.jpg differ diff --git a/nextjs/public/static/identity/BenWardAvatar48.jpg b/nextjs/public/static/identity/BenWardAvatar48.jpg new file mode 100755 index 0000000..3de55af Binary files /dev/null and b/nextjs/public/static/identity/BenWardAvatar48.jpg differ diff --git a/nextjs/public/static/identity/BenWardAvatar48.png b/nextjs/public/static/identity/BenWardAvatar48.png new file mode 100755 index 0000000..b0931ce Binary files /dev/null and b/nextjs/public/static/identity/BenWardAvatar48.png differ diff --git a/nextjs/public/static/identity/BenWardAvatarWide.png b/nextjs/public/static/identity/BenWardAvatarWide.png new file mode 100755 index 0000000..d1f9d24 Binary files /dev/null and b/nextjs/public/static/identity/BenWardAvatarWide.png differ diff --git a/nextjs/public/static/projects/tcp-header/detail.xht b/nextjs/public/static/projects/tcp-header/detail.xht new file mode 100755 index 0000000..3647d5b --- /dev/null +++ b/nextjs/public/static/projects/tcp-header/detail.xht @@ -0,0 +1,127 @@ + + + + The TCP Header » Detailed Information + + + + + + + + + + + + + + + + + + +

+

TCP Header Diagram

+

Additional Detail

+

Source and Destination Ports 

+

A port is a virtual entity to allow simultaneous TCP services to operate on the same physical hardware. The name is taken from the analogy with a ‘port’ or ‘plug’ that may be found on the back of a computer.

+

Consider a small business situation, where a local area network centres of a single network server, performing a variety of network services. The company intranet requires that a web server is running along with a database server for content. Company email passes through the send/fetch mail daemons, while company internet access is logged and filtered using a Web Proxy. The server itself can be remotely maintained from anywhere in the LAN using the SSH remote access protocol.

+

All of the above provide a network service, but they are able to co-exist because each operates on a unique port.

+ +

In a web browser, connecting to a website on a non-standard port can often take the format ‘http://www.example.com:8080’. However, ports are not related to the external network routing of TCP packets - that relates to the DNS resolution and the IP address stored in the IP header - on top of the TCP Header. Instead, ports provide a means for computers at each end of a network transaction to internally direct network packets to the appropriate service. Each unique port is maintained with its own data buffer, enabling each network service to operate independently.

+

In the above example, all network users will connect to the server machine. A user can browse an external Internet source through the web proxy on port 3126, while at the same time the system administrator can be directly manipulating the server through an SSH connection on port 22. While physically the services reside on the same machine, virtually (or ‘logically’) they are separate entities.

+

NB: While the example above may imply it, a service running on a single port is not restricted to a single user at one time. Multi-User behaviour is handled entirely by that service. Each service will have its own manner for handling multiple connections (for instance - Apache web server has a setting for ‘maximum number of client connections allowed at once’).

+

The TCP Header contains two Port items - Source and Destination. In a client-server situation like that above the default Destination port for a client connecting to the company web server will be ‘80‘. The Source port will be variable and assigned by the client browser or Operating System. A summary of client connections on a Windows or Unix machine can be found by running netstat -a -n -o -p tcp from the command prompt. This returns:-

+ +
D:\bmpw>netstat -a -n -o -p tcp
+Active Connections Proto Local Address     Foreign Address    State
+                     TCP 10.1.103.166:2020 213.129.226.165:80 CLOSE_WAIT
+                     TCP 10.1.103.166:4303 207.46.110.40:80   ESTABLISHED
+                     TCP 10.1.103.166:4438 207.46.110.26:80   ESTABLISHED
+                     TCP 127.0.0.1:1437    127.0.0.1:1438     ESTABLISHED
+                     TCP 127.0.0.1:1438    127.0.0.1:1437     ESTABLISHED
+ +

In the above trimmed output, it can be seen that as well as using ports to connect to remote services (where the local address is listed as 10.1.103.166, the network host IP of the machine) ports are also used to run local services - such as those running on ports 1437 and 1438 on localhost (ip:127.0.0.1).

+ +

Checksum 

+

The checksum of the TCP header is an error detection method. +It uses a method in which a 16 bit number is passed based on the +number of bits in the message (The header and the data). The +receiving machine can then sum the total of the 16 bit words and +if the total of this ones compliment number is zero +(1111111111111111) then the message can be considered correct. +If the sum of them is not zero then the message can be considered +to have lost its integrity during transfer. This method can not +correct the data; it can only tell if it has lost its +integrity.

+

While calculating the checksum the field of the checksum is +set to zero. The sum of all the 16 bit words in the TCP segment +header and TCP segment data is taken and then inverted (The +1’s compliment of it taken). If there are not enough bits +then zeros are used to pad the data out so that a checksum can +still be made. This is then put into the checksum field of the +TCP header.

+

Example

+

If we were to come up with the following words from the TCP +segment Header and TCP segment data together:

+
Word 1 : ABCD
+Word 2 : 1903
+Word 3 : 1984
+Word 4 : 5744
+Checksum : 0000 (Set to 0 while calculating)
+ +

The ones compliment sum of these numbers gives us:

+ +
Word 1 : 1010101111001101
+Word 2 : 0001100100000011
+Word 3 : 0001100110000100
+Word 4 : 0101011101000100
+Checksum : 0000000000000000
+ ----------------
+Sum : 0011010110011000
+Inverted : 1100101001100111(1’s compliment)
+  ----------------
+ +

When this is received the receiving computer +would get the 5 words and completes the following sum:

+
Word 1:1010101111001101
+Word 2:0001100100000011
+Word 3:0001100110000100
+Word 4:0101011101000100
+Checksum:1100101001100111
+               ----------------
+1111111111111111
+

Because we have got zero from the sum of the +words we can treat this as the data being sent has been sent +correctly and has its integrity.

+ +

Window 

+

The Window field is used in flow control, in order to ensure that the sender does not send too much data for the receiver to keep up with. The field is 16 bits long, and contains the number of data octets relative to the sequence number in the Acknowledgement Number field, that can be sent.

+

Each segment is assigned a Sequence Number, and a ‘window’ is placed over the stream, see below (a)

+

As the first segment is sent, the trailing edge of the window contracts (b)

+

As the acknowledgement for segment 0 is received, the leading edge of the window expands. (c). In this way, the window moves along the bit stream, and restricts the number of segments which can be sent at any time to those inside the window.

+

This allows the sender to continually send data as long as the window is ‘open’, and can send more than one segment before receiving acknowledgement (d) and (e)

+ + + + +
+ diff --git a/nextjs/public/static/projects/tcp-header/images/arrow.png b/nextjs/public/static/projects/tcp-header/images/arrow.png new file mode 100755 index 0000000..37084fa Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/arrow.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/bg_diagram.png b/nextjs/public/static/projects/tcp-header/images/bg_diagram.png new file mode 100755 index 0000000..4e5adeb Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/bg_diagram.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/bg_globe.png b/nextjs/public/static/projects/tcp-header/images/bg_globe.png new file mode 100755 index 0000000..42fd76f Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/bg_globe.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/cell_off.png b/nextjs/public/static/projects/tcp-header/images/cell_off.png new file mode 100755 index 0000000..cc964b0 Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/cell_off.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/cell_on.png b/nextjs/public/static/projects/tcp-header/images/cell_on.png new file mode 100755 index 0000000..318dcc2 Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/cell_on.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/diagram.png b/nextjs/public/static/projects/tcp-header/images/diagram.png new file mode 100755 index 0000000..9678c0f Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/diagram.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/originals/cell_gradient.psd b/nextjs/public/static/projects/tcp-header/images/originals/cell_gradient.psd new file mode 100755 index 0000000..168ffba Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/originals/cell_gradient.psd differ diff --git a/nextjs/public/static/projects/tcp-header/images/ports.gif b/nextjs/public/static/projects/tcp-header/images/ports.gif new file mode 100755 index 0000000..0e07c8f Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/ports.gif differ diff --git a/nextjs/public/static/projects/tcp-header/images/ports.png b/nextjs/public/static/projects/tcp-header/images/ports.png new file mode 100755 index 0000000..f939b1f Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/ports.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/screenshots/01.png b/nextjs/public/static/projects/tcp-header/images/screenshots/01.png new file mode 100755 index 0000000..7a6ad92 Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/screenshots/01.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/screenshots/02.png b/nextjs/public/static/projects/tcp-header/images/screenshots/02.png new file mode 100755 index 0000000..c8ca9ad Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/screenshots/02.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/screenshots/03.png b/nextjs/public/static/projects/tcp-header/images/screenshots/03.png new file mode 100755 index 0000000..61b0d77 Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/screenshots/03.png differ diff --git a/nextjs/public/static/projects/tcp-header/images/window.png b/nextjs/public/static/projects/tcp-header/images/window.png new file mode 100755 index 0000000..02371e8 Binary files /dev/null and b/nextjs/public/static/projects/tcp-header/images/window.png differ diff --git a/nextjs/public/static/projects/tcp-header/index.xht b/nextjs/public/static/projects/tcp-header/index.xht new file mode 100755 index 0000000..fb5e343 --- /dev/null +++ b/nextjs/public/static/projects/tcp-header/index.xht @@ -0,0 +1,273 @@ + + + + + + The TCP Header + + + + + + + + + + + + + +

The TCP Header

+ + + + + + +

A guide to the TCP Header Diagram: 

+ +
+ +

Please refer to Data and Computer Communications - 7th Edition (William Stallings) + Figure 20.10 "TCP Header", or view this copy.

+
+
+ +
+ + +
+
+ +

Back To Diagram

+
+

Source Port

+

A port is a virtual entity to allow simultaneous TCP services to operate + on the same physical hardware. The Source port will be variable and assigned + by the client browser or Operating System.

+

More...

+
+
+ +
+
+ +

Back To Diagram

+
+

Destination Port

+

A port is a virtual entity to allow simultaneous TCP services to operate on the same physical hardware. The destination port will be dependant on the server.

+

More...

+
+
+ + +
+
+ +

Back To Diagram

+
+

Sequence Number

+

Identifies the first byte of data in which the segment represents. If a connection is established (i.e. SYN flag is set) the sequence number for the first byte is the sequence number +1.

+
+
+ + +
+
+ +

Back To Diagram

+
+

Acknowledgement Number

+

Identifies the next sequence number that the sender expects to receive. This field is only used if the ACK (acknowledgement) flag is set.

+
+
+ + +
+
+ +

Back To Diagram

+
+

Data Offset

+

The number of 32-bit words in the TCP header.

+
+
+ +
+
+ +

Back To Diagram

+
+

Reserved

+

Reserved for future use. Not used at the present time.

+
+
+ +
+
+ +

Back To Diagram

+
+

Urgent

+

When set to 1 informs the receiver that the following data contains urgent data, and that the Urgent Pointer field contains valid data. If this pointer is set to 0, then the Urgent Pointer field is ignored.

+
+
+ +
+
+ +

Back To Diagram

+
+

Acknowledgement

+

When set to 1 tells the receiver that acknowledgement of receipt is required. this is usually set to 1

+
+
+ +
+
+ +

Back To Diagram

+
+

Push

+

When set to 1 instructs transmitter to send all outstanding data

+
+
+ +
+
+ +

Back To Diagram

+
+

Reset

+

When set to 1, reset the connection

+
+
+ +
+
+ +

Back To Diagram

+
+

Synchronised

+

When set to 1, synchronise sequence numbers

+
+
+ +
+
+ +

Back To Diagram

+
+

Finished

+

When set to 1 no more data from sender

+
+
+ +
+
+ +

Back To Diagram

+
+

Window

+

The Window field is used in flow control, in order to ensure that the sender does not send too much data for the receiver to keep up with. The field is 16 bits long, and contains the number of data octets relative to the sequence number in the Acknowledgement Number field, that can be sent.

+

More...

+
+
+ + +
+
+ +

Back To Diagram

+
+

Checksum

+

The checksum of the TCP header is an error detection method. If the sum of the 16 bit words of the message is not zero then the message can be considered to have lost its integrity during transfer. This method can not correct the data; only tell if it has lost its integrity.

+

More...

+
+
+ +
+
+ +

Back To Diagram

+
+

Urgent Pointer

+

The 16-bit urgent pointer is used to inform the receiver how much urgent information is being sent. It is added to segment sequence number to give the last segment of urgent data.

+
+
+ + +
+
+ +

Back To Diagram

+
+

Options & Padding

+

TCP options are used to convey further information in TCP headers. Currently there are 26 defined options and they fall into two classes; single octet and multi-octet options. An example TCP option defined, specifies the Maximum Segment Size (MSS) and consists of an option indicator (value 2 octet) followed by a data length octet (defined as value 4). The next two octets specify the actual MSS value itself.

+

TCP options may be separated by a NOOP (No-Operation) octet, defined as value 1. It is used to pad options to a word (32bit) boundary. Finally, the TCP options are terminated by an ‘End of Option List’ octet (value 0).

+

Padding is simply a sequence of zeros at the end of the TCP header. It ensures that the packet data is aligned to the start of a 32bit (word) boundary

+
+
+
+ + + +
+ diff --git a/nextjs/public/static/projects/tcp-header/style/Copy of tcp_diagram.css b/nextjs/public/static/projects/tcp-header/style/Copy of tcp_diagram.css new file mode 100755 index 0000000..ec2a052 --- /dev/null +++ b/nextjs/public/static/projects/tcp-header/style/Copy of tcp_diagram.css @@ -0,0 +1,250 @@ +/*8 Appearence and Layout Style Sheet for TCP Header Diagram */ +@media all { + .hidden { + display: none; + } +} + +@media screen { + body { + font-family: Verdana, Tahoma, sans-serif; + font-size: 1em; + border-bottom: 1px solid #000; + margin: 0px; + + color: #000; + background: #FFF; + + background-image: url("../images/bg_globe.png"); + background-repeat: no-repeat; + background-position: bottom right; + } + + #bodytext { + margin-left: 10%; + padding-left: 5px; padding-right: 5px; + border-left: 3px groove #CCC; + } + .screen-hide { + display: none; + visibility: hidden; + } + + object.fs { + width: 95%; + height: auto; + } + + /** Layout the Diagram */ + #diagram { + width: 95%; max-width: 800px; + margin-left: auto; margin-right: auto; + margin-top: 10px; margin-bottom: 10px; + background: #E6D900; + border: 1px solid #000; + z-index: 0; + } + /** Rows */ + #diagram div.diagram_row { + position: relative; + height: 2.5em; + } + + /** Hidden Cell Outer */ + #diagram div.diagram_row div.cell { + cursor: pointer; + height: 2.5em; + z-index: 1; + } + /** Hidden Cell Inner */ + #diagram div.diagram_row div.cell div { + background: #E6D900; + background-image: url("../images/cell_off.png"); + background-repeat: repeat-x; + background-position: bottom left; + text-align: center; + vertical-align: middle; + border: 1px solid #000; + + height: inherit; + z-index: 1; + } + + /** Hover Colour Change */ + #diagram div div.cell:hover div { + background: #CAD8DD; + background-image: url("../images/cell_on.png"); + background-repeat: repeat-x; + background-position: bottom left; + } + /** Collapse Content */ + #diagram div div.cell div .btt, #diagram div div.cell div p { + display: none; + visibility: hidden; + } + + #diagram div div.cell div h3 { + margin: 0px; + } + /** Expanded Cell Outer */ + #diagram div div.expCell { + cursor: default; + border: 1px solid #000; + border-top: 0px; + margin-top: 10px; + min-width: 50%; + /** On top */ + z-index: 2 !important; + } + + /** Expanded Cell Inner */ + #diagram div div.expCell div { + background: #CAD8DD; + background-image: url("../images/cell_on.png"); + background-repeat: repeat-x; + background-position: bottom left; + + border-top: 1px solid #000; + padding: 5px; + overflow: scroll; + min-height: 8em !important; + max-height: 10em !important; + /** On top */ + z-index: 2 !important; + } + #diagram div div.expCell h3 { + font-size: 100% !important; + text-align: left !important; + } + + /** Override Small cells spans when expanded */ + #diagram div div.expCell span { + display: inline !important; + text-transform: none !important; + } + + /** Small cells with vertical text */ + #diagram div div.smallcell { + padding: 0px; + } + #diagram div div.smallcell div h3 { + font-size: 50%; + } + #diagram div div.smallcell span.vert { + display:block; + padding: 0px; margin: 0px; + text-transform: uppercase; + } + #diagram div div.smallcell span.trim { + display:none; + } + + /** Unique Cells -- Positioning only */ + #diagram div #cell_sourceport { + position: absolute; + left: 0%; + width: 50%; + } + #diagram div #cell_destinationport { + position: absolute; + left: 50%; + width: 50%; + } + #diagram div #cell_seqnum { + position: absolute; + left: 0%; + width: 100%; + } + #diagram div #cell_acknum { + position: absolute; + left: 0%; + width: 100%; + } + #diagram div #cell_dataoffset { + position: absolute; + left: 0%; + width: 12.5%; + + font-size: 95%; + + } + #diagram div #cell_reserved { + position: absolute; + left: 12.5%; + width: 18.75%; + } + #diagram div #cell_urg { + position: absolute; + left: 31.25%; + width: 3.125%; + } + #diagram div #cell_ack { + position: absolute; + left: 34.375%; + width: 3.125%; + } + #diagram div #cell_psh { + position: absolute; + left: 37.5%; + width: 3.125%; + } + #diagram div #cell_rst { + position: absolute; + left: 40.625%; + width: 3.125%; + } + #diagram div #cell_syn { + position: absolute; + left: 43.75%; + width: 3.125%; + } + #diagram div #cell_fin { + position: absolute; + left: 46.875%; + width: 3.125%; + } + #diagram div #cell_window { + position: absolute; + left: 50%; + width: 50%; + } + #diagram div #cell_checksum { + position: absolute; + left: 0%; + width: 50%; + } + #diagram div #cell_urgpoint { + position: absolute; + left: 50%; + width: 50%; + } + #diagram div #cell_opt { + position: absolute; + left: 0%; + width: 100%; + } + /** Global (exp|colapsed) Diagram Styles */ + #diagram h3 { + font-size: 100%; + padding-top: 0.5em; padding-bottom: 0.4em; + margin: 0px; + } + #diagram small { + } + #diagram .btt { + float: right; + } + +} +/** Printer Friendly Version */ +@media print { + body { + background: #FFF; + font-family: Centaur, Georgia, sans-serif; + } + /** Hide "Back To Top"/"Close" items */ + .btt { + display: none; + visibility: hidden; + } +} \ No newline at end of file diff --git a/nextjs/public/static/projects/tcp-header/style/tcp_diagram.css b/nextjs/public/static/projects/tcp-header/style/tcp_diagram.css new file mode 100755 index 0000000..7c8601b --- /dev/null +++ b/nextjs/public/static/projects/tcp-header/style/tcp_diagram.css @@ -0,0 +1,306 @@ +/*8 Appearence and Layout Style Sheet for TCP Header Diagram */ +@media all { + body { + font-size: 1em; + } + #footer { + float: right; + text-align: right; + + width: 50%; + min-width: 500px; + font-size: 80%; + border-top: 1px solid #EEE; + } + + .hidden { + display: none; + } + + pre { + display: block; + margin-left: 20px; margin-right: 20px; + padding: 5px; + border: 1px solid #EEE; + } + + span.pre { + font-family: "courier new", courier, typewriter, fixed; + } + + .right { + float: right; + } +} + +@media screen { + body { + font-family: Verdana, Tahoma, sans-serif; + font-size: 1em; + margin: 0px; + + color: #000; + background: #FFF; + + background-image: url("../images/bg_globe.png"); + background-repeat: no-repeat; + background-position: bottom right; + } + + #bodytext { + margin-left: 15px; + padding-left: 5px; padding-right: 5px; + border-left: 3px solid #EEE; + } + + #iemsg * { + font-size: 75%; + } + + #menu { + position: absolute; + top: 10px; + left: 5px; + width: 100px; + + font-size: 80%; + border: 1px solid #EEE; + } + + #menu ul, #menu li { + display: block; + margin: 0px; + padding: 0px; + } + .screen-hide { + display: none; + } + + object.fs { + width: 95%; + height: auto; + } + + /** Layout the Diagram */ + #diagram { + position: relative; + width: 95%; max-width: 800px; + height: 15em; + margin-left: auto; margin-right: auto; + margin-top: 10px; margin-bottom: 10px; + background: #E6D900; + border: 1px solid #000; + z-index: 0; + } + + /** Hidden Cell Outer */ + #diagram div.cell { + cursor: pointer; + height: 2.5em; + z-index: 1; + } + /** Hidden Cell Inner */ + #diagram div.cell div { + background: #E6D900; + background-image: url("../images/cell_off.png"); + background-repeat: repeat-x; + background-position: bottom left; + text-align: center; + vertical-align: middle; + border: 1px solid #000; + + height: inherit; + z-index: 1; + } + + /** Hover Colour Change */ + #diagram div.cell:hover div { + background: #CAD8DD; + background-image: url("../images/cell_on.png"); + background-repeat: repeat-x; + background-position: bottom left; + } + /** Collapse Content */ + #diagram div.cell div .btt, #diagram div.cell div p { + display: none; + visibility: hidden; + } + + #diagram div.cell div h3 { + margin: 0px; + } + /** Expanded Cell Outer */ + #diagram div.expCell { + cursor: default; + border: 1px solid #000; + border-top: 0px; + margin-top: 10px; + min-width: 50%; + /** On top */ + z-index: 2 !important; + } + + /** Expanded Cell Inner */ + #diagram div.expCell div { + background: #CAD8DD; + background-image: url("../images/cell_on.png"); + background-repeat: repeat-x; + background-position: bottom left; + + border-top: 1px solid #000; + padding: 5px; + overflow: scroll; + min-height: 8em !important; + max-height: 10em !important; + /** On top */ + z-index: 2 !important; + } + #diagram div.expCell h3 { + font-size: 100% !important; + text-align: left !important; + } + + /** Override Small cells spans when expanded */ + #diagram div.expCell span { + display: inline !important; + text-transform: none !important; + } + + /** Small cells with vertical text */ + #diagram div.smallcell { + padding: 0px; + } + #diagram div.smallcell div h3 { + font-size: 50%; + } + #diagram div.smallcell span.vert { + display:block; + padding: 0px; margin: 0px; + text-transform: uppercase; + } + #diagram div.smallcell span.trim { + display:none; + } + + /** Unique Cells -- Positioning only */ + #diagram #cell_sourceport { + position: absolute; + left: 0%; + width: 50%; + } + #diagram #cell_destinationport { + position: absolute; + left: 50%; + width: 50%; + } + #diagram #cell_seqnum { + position: absolute; + top: 2.5em; + left: 0%; + width: 100%; + } + #diagram #cell_acknum { + position: absolute; + top: 5em; + left: 0%; + width: 100%; + } + #diagram #cell_dataoffset { + position: absolute; + top: 7.5em; + left: 0%; + width: 12.5%; + } + #diagram #cell_dataoffset h3 { + font-size: 80%; + } + #diagram #cell_reserved { + position: absolute; + top: 7.5em; + left: 12.5%; + width: 18.75%; + } + #diagram #cell_urg { + position: absolute; + top: 7.5em; + left: 31.25%; + width: 3.125%; + } + #diagram #cell_ack { + position: absolute; + top: 7.5em; + left: 34.375%; + width: 3.125%; + } + #diagram #cell_psh { + position: absolute; + top: 7.5em; + left: 37.5%; + width: 3.125%; + } + #diagram #cell_rst { + position: absolute; + top: 7.5em; + left: 40.625%; + width: 3.125%; + } + #diagram #cell_syn { + position: absolute; + top: 7.5em; + left: 43.75%; + width: 3.125%; + } + #diagram #cell_fin { + position: absolute; + top: 7.5em; + left: 46.875%; + width: 3.125%; + } + #diagram #cell_window { + position: absolute; + top: 7.5em; + left: 50%; + width: 50%; + } + #diagram #cell_checksum { + position: absolute; + top: 10em; + left: 0%; + width: 50%; + } + #diagram #cell_urgpoint { + position: absolute; + top: 10em; + left: 50%; + width: 50%; + } + #diagram #cell_opt { + position: absolute; + top: 12.5em; + left: 0%; + width: 100%; + } + /** Global (exp|colapsed) Diagram Styles */ + #diagram h3 { + font-size: 100%; + padding-top: 0.5em; padding-bottom: 0.4em; + margin: 0px; + } + #diagram small { + } + #diagram .btt { + float: right; + } + +} +/** Printer Friendly Version */ +@media print { + body { + background: #FFF; + font-family: Georgia, sans-serif; + } + /** Hide "Back To Top"/"Close" items */ + #diagram .btt, #menu, #iemsg { + display: none; + } +} \ No newline at end of file diff --git a/nextjs/tsconfig.json b/nextjs/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/nextjs/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/nextjs/types/textile-js.d.ts b/nextjs/types/textile-js.d.ts new file mode 100644 index 0000000..63ac0f4 --- /dev/null +++ b/nextjs/types/textile-js.d.ts @@ -0,0 +1,4 @@ +declare module "textile-js" { + function textile(input: string): string; + export default textile; +} diff --git a/nextjs/vitest.config.ts b/nextjs/vitest.config.ts new file mode 100644 index 0000000..8312240 --- /dev/null +++ b/nextjs/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname), + }, + }, + test: { + environment: "jsdom", + include: ["**/*.test.ts", "**/*.test.tsx"], + css: { + modules: { + classNameStrategy: "non-scoped", + }, + }, + }, +}); diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..98e958e --- /dev/null +++ b/plan.md @@ -0,0 +1,307 @@ +# Jekyll to Next.js Migration Plan + +## Overview + +Port benward.uk from Jekyll 4 to Next.js (App Router) with TypeScript. All existing content files remain unchanged in their current structure. The site renders server-side with JIT (on-demand) rendering — no static export. All new TypeScript code has tests. + +--- + +## 1. Project Scaffold + +Create a new `nextjs/` directory alongside the existing `jekyll/` directory. + +``` +benward-web/ +├── jekyll/ # existing, untouched +├── nextjs/ # new Next.js app +│ ├── app/ # App Router pages +│ ├── components/ # React components (layouts, includes), each with colocated tests +│ ├── config/ # Site configuration (ported from _config.yml) +│ ├── lib/ # Data fetching, plugins, utilities, each with colocated tests +│ ├── public/ # Static assets (copied from jekyll/static, jekyll/images) +│ ├── next.config.ts +│ ├── tsconfig.json +│ ├── package.json +│ └── jest.config.ts +``` + +**Content reference**: The Next.js app reads content directly from `../jekyll/_posts/` and other content files. No content duplication. + +--- + +## 2. Content Library (`lib/`) + +### `lib/content.ts` — Post loading and parsing +- Read all `.md` and `.textile` files from `../jekyll/_posts/blog/` +- Parse YAML frontmatter with `gray-matter` +- Render Markdown with `unified`/`remark`/`rehype` pipeline +- Render Textile with `textile-js` (or equivalent) +- Extract and normalize all frontmatter fields: + - `title`, `date`, `updated`, `summary`, `tags`, `category`, `layout` + - `geo` (name, latitude, longitude) + - `canonical`, `atomid`, `original_service`, `original_url`, `tumblr_post_type` +- Preserve timezone information from date strings (store as ISO 8601 strings, not JS Date objects) +- Sort posts by date descending + +### `lib/archives.ts` — Archive index generation +- Group posts by year and month +- Generate archive period metadata (previous/next navigation, post counts) +- Provide functions: `getYearArchive(year)`, `getMonthArchive(year, month)`, `getAllArchivePeriods()` + +### `lib/posts.ts` — Post enrichment (port of `jekyl_post.rb`) +- `cleanUrl(post)` — strip `.html` extension +- `githubSourceUrl(post)` — generate GitHub edit link from slug/path +- `globalDate(post)` — preserve timezone from frontmatter date +- `generateTitle(post)` — auto-generate title from date for untitled Tumblr imports +- `getExcerpt(post)` — use `summary` frontmatter or auto-generate + +### `lib/base60.ts` — Base60 encoding (port of `base60.rb`) +- Port NewBase60 encoding algorithm to TypeScript +- `encode(timestamp: number): string` +- Used for shortlink generation: `https://bnwrd.me/{base60}` + +### `lib/romans.ts` — Roman numeral conversion (port of `romans.rb`) +- `romanize(year: number): string` +- Used in footer copyright display + +### `lib/tag-id.ts` — Atom tag URI generation (port of `tag_id.rb`) +- `generateTagId(post): string` +- RFC 4151 tag URIs using domain-switching logic (benward.me pre-2018, benward.uk 2018+) + +### `lib/dates.ts` — Date formatting utilities (port of `liquid_standard_filters.rb`, `jekyll_utils.rb`) +- Timezone-preserving date formatting +- ISO format, display format, time-only format matching `_config.yml` patterns +- Uses `date-fns` or `luxon` for formatting with timezone support + +### `config/site.ts` — Site configuration +- Centralize all config values currently in `_config.yml`: + - `title`, `url`, `shortdomain`, `author`, `github_slug`, `git_base` + - Date format strings + - Permalink structure +- Lives in `config/` directory, not `lib/` + +--- + +## 3. App Router URL Structure (`app/`) + +All routes use dynamic server rendering (no `generateStaticParams` — JIT only). + +### Routes + +``` +app/ +├── layout.tsx # Root layout (port of base.html) +├── page.tsx # Homepage: / (port of index.html) +├── blog/ +│ └── [slug]/ +│ └── page.tsx # Blog post: /blog/{slug} +│ └── route-redirects handled via next.config redirects for .html +├── [year]/ +│ ├── page.tsx # Year archive: /2024/ +│ └── [month]/ +│ └── page.tsx # Month archive: /2024/01/ +├── about/ +│ └── page.tsx # /about +├── network/ +│ └── page.tsx # /network +├── feeds/ +│ └── page.tsx # /feeds +├── feed.atom/ +│ └── route.ts # Atom feed (Route Handler) +├── robots.txt/ +│ └── route.ts # robots.txt (Route Handler) +├── sitemap.xml/ +│ └── route.ts # sitemap (Route Handler) +├── humans.txt/ +│ └── route.ts # humans.txt (Route Handler) +└── not-found.tsx # 404 page +``` + +### `.html` Redirects + +In `next.config.ts`, add redirect rules: +```ts +redirects: [ + { source: '/blog/:slug.html', destination: '/blog/:slug', permanent: true }, + // etc. +] +``` + +--- + +## 4. React Components (`components/`) + +Port Jekyll layouts and includes to React server components. Each component lives in its own subdirectory with colocated styles, tests, and barrel export: + +``` +components/Cover/ +├── Cover.tsx +├── Cover.module.css +├── Cover.test.tsx +└── index.ts # re-exports Cover +``` + +### Layout Components +- **`RootLayout`** — port of `base.html`: HTML shell, meta tags, header, footer (with Roman numeral year), nav +- **`BlogPostLayout`** — port of `blog.html`: article with h-entry microformat, dateline, geo, tags, prev/next nav +- **`ArticleLayout`** — port of `article.html`: simple article wrapper +- **`ArchiveMonthLayout`** — port of `archive_month.html`: monthly h-feed with navigation +- **`ArchiveYearLayout`** — port of `archive_year.html`: yearly h-feed organized by month + +### Include Components +- **`Cover`** — port of `cover.html`: bio card with h-card +- **`Identity`** — port of `identity.html`: microformat profile links +- **`PostSummary`** — port of `post_summary.html`: article summary for listings +- **`ArchiveNavigation`** — port of `archive_navigation.html`: prev/next archive period nav +- **`Share`** — port of `share.html`: permalink, shortlink, GitHub source link +- **`TwitterMeta`** — port of `twitter.html`: Twitter Card meta tags +- **`Scripts`** — port of `scripts.html`: Gauges analytics, Twitter widgets +- **`Profiles`** — port of `profiles.html`: microformat profile links + +--- + +## 5. Static Assets & CSS (`public/`, CSS Modules) + +### Static files +Copy from Jekyll: +- `jekyll/static/` → `public/static/` +- `jekyll/images/` → `public/images/` + +### CSS Strategy +Decompose `jekyll/css/sixthree.css` into: +- **`lib/global.css`** — CSS variables/custom properties, base/reset styles, body/html defaults, typography. Imported in `app/layout.tsx`. +- **CSS Modules per component** — Each component directory includes a colocated `.module.css`: + - `components/Cover/Cover.module.css` — h-card/bio styles + - `components/PostSummary/PostSummary.module.css` — article summary styles + - `components/ArchiveNavigation/ArchiveNavigation.module.css` — archive nav flexbox + - `components/Share/Share.module.css` — share/link section styles + - `components/BlogPostLayout/BlogPostLayout.module.css` — h-entry, dateline, geo, tag list styles + - `components/ArchiveMonthLayout/ArchiveMonthLayout.module.css` — monthly archive styles + - `components/ArchiveYearLayout/ArchiveYearLayout.module.css` — yearly archive styles + - `app/layout.module.css` — header, footer, page canvas, site nav + +Microformat class names (`.h-entry`, `.p-name`, etc.) remain as plain classes in the global CSS since they're semantic markers, not styling hooks. Component-specific visual styles use CSS Modules. + +--- + +## 6. Atom Feed (`app/feed.atom/route.ts`) + +Route Handler that: +- Fetches 20 most recent blog posts +- Generates Atom XML with shortlinks (base60), tag URIs, canonical URLs +- Returns `Response` with `Content-Type: application/atom+xml` +- Ports all logic from `jekyll/feed.atom` + +--- + +## 7. Testing Strategy + +Use Jest + React Testing Library. **Tests are colocated with their source files** (e.g., `lib/base60.ts` → `lib/base60.test.ts`, `components/Cover.tsx` → `components/Cover.test.tsx`). + +### Library Tests (colocated in `lib/`) +- `lib/base60.test.ts` — encode/decode correctness, edge cases +- `lib/romans.test.ts` — Roman numeral conversion +- `lib/tag-id.test.ts` — tag URI generation with domain switching +- `lib/dates.test.ts` — timezone-preserving formatting +- `lib/content.test.ts` — frontmatter parsing, Markdown/Textile rendering +- `lib/posts.test.ts` — post enrichment (clean URLs, GitHub links, title generation) +- `lib/archives.test.ts` — archive grouping, period navigation + +### Config Tests (colocated in `config/`) +- `config/site.test.ts` — config values + +### Component Tests (colocated in each component's subdirectory) +- e.g. `components/PostSummary/PostSummary.test.tsx` +- Test key components render correct microformat markup +- Test archive navigation generates correct links +- Test post summary handles canonical URLs + +### Integration Tests +- Test Atom feed generation produces valid XML +- Test archive routes return correct post groupings + +--- + +## 8. Implementation Order + +### Phase 1: Foundation +1. Initialize Next.js project with TypeScript +2. Set up Jest testing +3. Implement `config/site.ts` +4. Implement `lib/base60.ts` + tests +5. Implement `lib/romans.ts` + tests +6. Implement `lib/dates.ts` + tests +7. Implement `lib/tag-id.ts` + tests + +### Phase 2: Content Pipeline +8. Implement `lib/content.ts` (frontmatter parsing, Markdown rendering, Textile rendering) + tests +9. Implement `lib/posts.ts` (post enrichment) + tests +10. Implement `lib/archives.ts` (archive grouping/navigation) + tests + +### Phase 3: Components & Layouts +11. Copy static assets to `public/` +12. Build `RootLayout` (base.html port) with all include components +13. Build `BlogPostLayout` and blog post page route +14. Build archive page routes with layout components +15. Build homepage +16. Build static pages (about, network, feeds) + +### Phase 4: Feeds & Metadata +17. Build Atom feed route handler +18. Build robots.txt, sitemap.xml, humans.txt route handlers +19. Add `.html` redirect rules in next.config.ts +20. Add Twitter Card / Open Graph meta tags + +### Phase 5: Polish & Verification +21. Component tests for microformat correctness +22. Integration tests for feed and archive generation +23. Manual verification of URL structure against Jekyll output +24. Verify all frontmatter-driven features work (canonical URLs, geo, Tumblr imports) + +--- + +## Key Dependencies + +```json +{ + "dependencies": { + "next": "^15", + "react": "^19", + "react-dom": "^19", + "gray-matter": "^4", + "unified": "^11", + "remark-parse": "^11", + "remark-rehype": "^11", + "rehype-stringify": "^10", + "rehype-raw": "^7", + "textile-js": "^2", + "date-fns": "^4", + "date-fns-tz": "^3" + }, + "devDependencies": { + "typescript": "^5", + "@types/react": "^19", + "@types/node": "^22", + "jest": "^29", + "ts-jest": "^29", + "@testing-library/react": "^16", + "@testing-library/jest-dom": "^6" + } +} +``` + +--- + +## Design Decisions + +1. **JIT rendering, not static**: All pages use `force-dynamic` or default dynamic rendering. No `generateStaticParams`. This matches the requirement for JIT rendering over static builds. + +2. **Content stays in Jekyll directory**: Posts are read from `../jekyll/_posts/` — no content duplication. The Jekyll site can continue to work alongside. + +3. **Server components by default**: All components are React Server Components unless interactivity is needed (unlikely for this blog). + +4. **Timezone preservation**: Dates stored as ISO 8601 strings throughout, never converted to local time. This is critical — the Jekyll site has custom plugins specifically for this. + +5. **Microformat fidelity**: All h-entry, h-card, h-feed, h-geo class names preserved exactly as in the Jekyll templates. + +6. **CSS Modules**: The existing `sixthree.css` is decomposed into CSS Modules per component, with global styles (variables, resets, typography, microformat classes) in `lib/global.css`.