From 71a072b0beb948d44e0b6c1101df105c32653809 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Sun, 22 Jun 2025 14:38:36 -0700 Subject: [PATCH] file viewer work --- framework/backend/entry-node.ts | 4 +- framework/engine/jsx-runtime.ts | 18 +++-- framework/engine/marko-runtime.ts | 7 +- framework/engine/ssr.ts | 20 +++++- framework/hot.ts | 1 + src/backend.ts | 1 - src/file-viewer/backend.tsx | 16 ++++- src/file-viewer/format.ts | 40 ++++++++--- src/file-viewer/pages/file.404.tsx | 34 +++++++++ .../pages/file.cotyledon_speedbump.tsx | 2 +- src/file-viewer/sort.ts | 58 +++++++++++++++ src/file-viewer/views/canvas.astro | 12 ---- src/file-viewer/views/canvas.marko | 17 +++++ src/file-viewer/views/clofi.css | 9 +++ src/file-viewer/views/clofi.tsx | 54 ++------------ src/file-viewer/views/lofi.css | 43 +++++++++++ src/file-viewer/views/lofi.marko | 71 +++++++++++++++++++ src/file-viewer/views/lofideon.marko | 22 ++++++ tsconfig.json | 2 +- 19 files changed, 343 insertions(+), 88 deletions(-) create mode 100644 src/file-viewer/pages/file.404.tsx create mode 100644 src/file-viewer/sort.ts delete mode 100644 src/file-viewer/views/canvas.astro create mode 100644 src/file-viewer/views/canvas.marko create mode 100644 src/file-viewer/views/lofi.css create mode 100644 src/file-viewer/views/lofi.marko create mode 100644 src/file-viewer/views/lofideon.marko diff --git a/framework/backend/entry-node.ts b/framework/backend/entry-node.ts index 6c0a40d..5b1e877 100644 --- a/framework/backend/entry-node.ts +++ b/framework/backend/entry-node.ts @@ -3,7 +3,9 @@ import "#debug"; const protocol = "http"; -const server = serve(app, ({ address, port }) => { +const server = serve({ + fetch: app.fetch, +}, ({ address, port }) => { if (address === "::") address = "::1"; console.info(url.format({ protocol, diff --git a/framework/engine/jsx-runtime.ts b/framework/engine/jsx-runtime.ts index f1b5c73..141aaf2 100644 --- a/framework/engine/jsx-runtime.ts +++ b/framework/engine/jsx-runtime.ts @@ -17,13 +17,21 @@ export function jsxDEV( _key: string, // Unused with the clover engine _isStaticChildren: boolean, - // Unused with the clover engine - _source: unknown, + source: engine.SrcLoc, ): engine.Element { + const { fileName, lineNumber, columnNumber } = source; + + // Assert the component type is valid to render. if (typeof type !== "function" && typeof type !== "string") { - throw new Error("Invalid component type: " + engine.inspect(type)); + throw new Error( + `Invalid component type at ${fileName}:${lineNumber}:${columnNumber}: ` + + engine.inspect(type) + + ". Clover SSR element must be a function or string", + ); } - return [engine.kElement, type, props]; + + // Construct an `ssr.Element` + return [engine.kElement, type, props, "", source]; } // jsxs @@ -37,7 +45,7 @@ declare global { interface ElementChildrenAttribute { children: Node; } - type Element = engine.Node; + type Element = engine.Element; type ElementType = keyof IntrinsicElements | engine.Component; type ElementClass = ReturnType; } diff --git a/framework/engine/marko-runtime.ts b/framework/engine/marko-runtime.ts index 477847f..d87a2b8 100644 --- a/framework/engine/marko-runtime.ts +++ b/framework/engine/marko-runtime.ts @@ -123,14 +123,15 @@ export function escapeXML(input: unknown) { // The rationale of this check is that the default toString method // creating `[object Object]` is universally useless to any end user. if ( + input == null || (typeof input === "object" && input && // only block this if it's the default `toString` input.toString === Object.prototype.toString) ) { throw new Error( - `Unexpected object in template placeholder: '` + - engine.inspect({ name: "clover" }) + "'. " + - `To emit a literal '[object Object]', use \${String(value)}`, + `Unexpected value in template placeholder: '` + + engine.inspect(input) + "'. " + + `To emit a literal '${input}', use \${String(value)}`, ); } return marko.escapeXML(input); diff --git a/framework/engine/ssr.ts b/framework/engine/ssr.ts index 5b28f60..56ca992 100644 --- a/framework/engine/ssr.ts +++ b/framework/engine/ssr.ts @@ -79,6 +79,8 @@ export type Element = [ tag: typeof kElement, type: string | Component, props: Record, + _?: "", + source?: SrcLoc, ]; export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; /** @@ -88,6 +90,12 @@ export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; export type Component = ( props: Record, ) => Exclude; +/** Emitted by JSX runtime */ +export interface SrcLoc { + fileName: string; + lineNumber: number; + columnNumber: number; +} /** * Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are @@ -130,9 +138,15 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode { const { 1: tag, 2: props } = node; if (typeof tag === "function") { currentRender = r; - const result = tag(props); - currentRender = null; - return resolveNode(r, result); + try { + return resolveNode(r, tag(props)); + } catch (e) { + const { 4: src } = node; + if (e && typeof e === "object") { + } + } finally { + currentRender = null; + } } if (typeof tag !== "string") throw new Error("Unexpected " + typeof type); const children = props?.children; diff --git a/framework/hot.ts b/framework/hot.ts index b375e84..6b4f6a7 100644 --- a/framework/hot.ts +++ b/framework/hot.ts @@ -145,6 +145,7 @@ function loadEsbuildCode( target: "esnext", jsx: "automatic", jsxImportSource: "#ssr", + jsxDev: true, sourcefile: filepath, }).code; return module._compile(src, filepath, "commonjs"); diff --git a/src/backend.ts b/src/backend.ts index eb4d29f..bbb4585 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -11,7 +11,6 @@ app.use(admin.middleware); app.route("", require("./q+a/backend.ts").app); app.route("", require("./file-viewer/backend.tsx").app); -// app.route("", require("./friends/backend.tsx").app); app.use(assets.middleware); diff --git a/src/file-viewer/backend.tsx b/src/file-viewer/backend.tsx index c5b72d8..7cc5176 100644 --- a/src/file-viewer/backend.tsx +++ b/src/file-viewer/backend.tsx @@ -42,9 +42,15 @@ app.post("/file/cotyledon", async (c) => { }); app.get("/file/*", async (c, next) => { - if (c.req.header("User-Agent")?.toLowerCase()?.includes("discordbot")) { + const ua = c.req.header("User-Agent")?.toLowerCase() ?? ""; + const lofi = ua.includes("msie") || ua.includes("rv:") || false; + + // Discord ignores 'robots.txt' which violates the license agreement. + if (ua.includes("discordbot")) { return next(); } + console.log(ua, lofi); + let rawFilePath = c.req.path.slice(5) || "/"; if (rawFilePath.endsWith("$partial")) { return getPartialPage(c, rawFilePath.slice(0, -"$partial".length)); @@ -88,7 +94,7 @@ app.get("/file/*", async (c, next) => { } satisfies APIDirectoryList; return c.json(json); } - c.res = await renderView(c, "file-viewer/clofi", { + c.res = await renderView(c, `file-viewer/${lofi ? "lofi" : "clofi"}`, { file, hasCotyledonCookie, }); @@ -100,7 +106,11 @@ app.get("/file/*", async (c, next) => { if (c.req.query("dl") !== undefined) { viewMode = "download"; } - if (viewMode == undefined && c.req.header("Accept")?.includes("text/html")) { + if ( + viewMode == undefined && + c.req.header("Accept")?.includes("text/html") && + !lofi + ) { prefetchFile(file.path); c.res = await renderView(c, "file-viewer/clofi", { file, diff --git a/src/file-viewer/format.ts b/src/file-viewer/format.ts index 23b2e52..04debed 100644 --- a/src/file-viewer/format.ts +++ b/src/file-viewer/format.ts @@ -9,15 +9,15 @@ export function formatSize(bytes: number) { return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; } -export function formatDate(date: Date) { - // YYYY-MM-DD, format in PST timezone - return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); -} - -export function formatShortDate(date: Date) { - // YY-MM-DD, format in PST timezone - return formatDate(date).slice(2); -} +// export function formatDateDefined(date: Date) { +// // YYYY-MM-DD, format in PST timezone +// return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); +// } +// +// export function formatShortDate(date: Date) { +// // YY-MM-DD, format in PST timezone +// return formatDate(date).slice(2); +// } export function formatDuration(seconds: number) { const minutes = Math.floor(seconds / 60); @@ -255,5 +255,27 @@ export function highlightHashComments(text: string) { .join("\n"); } +const unknownDate = new Date("1970-01-03"); +const unknownDateWithKnownYear = new Date("1970-02-20"); + +export function formatDate(dateTime: Date) { + return dateTime < unknownDateWithKnownYear + ? ( + dateTime < unknownDate + ? ( + "??.??.??" + ) + : `xx.xx.${21 + Math.floor(dateTime.getTime() / 86400000)}` + ) + : ( + `${(dateTime.getMonth() + 1).toString().padStart(2, "0")}.${ + dateTime + .getDate() + .toString() + .padStart(2, "0") + }.${dateTime.getFullYear().toString().slice(2)}` + ); +} + import type { MediaFile } from "@/file-viewer/models/MediaFile.ts"; import { escapeHtml } from "#ssr"; diff --git a/src/file-viewer/pages/file.404.tsx b/src/file-viewer/pages/file.404.tsx new file mode 100644 index 0000000..74cae0b --- /dev/null +++ b/src/file-viewer/pages/file.404.tsx @@ -0,0 +1,34 @@ +import { MediaFile } from "../models/MediaFile.ts"; +import { MediaPanel } from "../views/clofi.tsx"; +import { addScript } from "#sitegen"; + +export const theme = { + bg: "#312652", + fg: "#f0f0ff", + primary: "#fabe32", +}; + +export const meta = { title: "file not found" }; + +export default function CotyledonPage() { + addScript("../scripts/canvas_cotyledon.client.ts"); + return ( +
+ +
+
+
+

this file does not exist ...

+

+ return +

+
+
+
+ ); +} diff --git a/src/file-viewer/pages/file.cotyledon_speedbump.tsx b/src/file-viewer/pages/file.cotyledon_speedbump.tsx index 934104b..2b08a5c 100644 --- a/src/file-viewer/pages/file.cotyledon_speedbump.tsx +++ b/src/file-viewer/pages/file.cotyledon_speedbump.tsx @@ -1,7 +1,7 @@ import { MediaFile } from "../models/MediaFile.ts"; -import { Speedbump } from "../cotyledon.tsx"; import { MediaPanel } from "../views/clofi.tsx"; import { addScript } from "#sitegen"; +import { Speedbump } from "../cotyledon.tsx"; export const theme = { bg: "#312652", diff --git a/src/file-viewer/sort.ts b/src/file-viewer/sort.ts new file mode 100644 index 0000000..cbbb8ba --- /dev/null +++ b/src/file-viewer/sort.ts @@ -0,0 +1,58 @@ +export function splitRootDirFiles(dir: MediaFile, hasCotyledonCookie: boolean) { + const children = dir.getPublicChildren(); + let readme: MediaFile | null = null; + + const groups = { + // years 2025 and onwards + years: [] as MediaFile[], + // named categories + categories: [] as MediaFile[], + // years 2017 to 2024 + cotyledon: [] as MediaFile[], + }; + const colorMap: Record = { + years: "#a2ff91", + categories: "#9c91ff", + cotyledon: "#ff91ca", + }; + for (const child of children) { + const basename = child.basename; + if (basename === "readme.txt") { + readme = child; + continue; + } + + const year = basename.match(/^(\d{4})/); + if (year) { + const n = parseInt(year[1]); + if (n >= 2025) { + groups.years.push(child); + } else { + groups.cotyledon.push(child); + } + } else { + groups.categories.push(child); + } + } + + let sections = []; + for (const [key, files] of Object.entries(groups)) { + if (key === "cotyledon" && !hasCotyledonCookie) { + continue; + } + if (key === "years" || key === "cotyledon") { + files.sort((a, b) => { + return b.basename.localeCompare(a.basename); + }); + } else { + files.sort((a, b) => { + return a.basename.localeCompare(b.basename); + }); + } + sections.push({ key, titleColor: colorMap[key], files }); + } + + return { readme, sections }; +} + +import { MediaFile } from "./models/MediaFile.ts"; diff --git a/src/file-viewer/views/canvas.astro b/src/file-viewer/views/canvas.astro deleted file mode 100644 index ca31307..0000000 --- a/src/file-viewer/views/canvas.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import { useInlineScript } from "../framework/page-resources.ts"; - -const { script } = Astro.props; -useInlineScript('canvas_' + script as any); -useInlineScript('canvas_demo'); ---- - diff --git a/src/file-viewer/views/canvas.marko b/src/file-viewer/views/canvas.marko new file mode 100644 index 0000000..323f4f6 --- /dev/null +++ b/src/file-viewer/views/canvas.marko @@ -0,0 +1,17 @@ +export interface Input { + script: string; +} +export const meta = { title: "canvas" }; + + + + + + +client import "./canvas.client.ts"; + +import { addScript as AddScript } from '#sitegen'; diff --git a/src/file-viewer/views/clofi.css b/src/file-viewer/views/clofi.css index ba0d4b1..4131638 100644 --- a/src/file-viewer/views/clofi.css +++ b/src/file-viewer/views/clofi.css @@ -687,6 +687,15 @@ h3 { transform: translateY(0); } } +.notfound { + p { + text-align: center; + } + a { + text-decoration: underline; + } +} + /* MOBILE */ @media (max-width: 1000px) { html, body { diff --git a/src/file-viewer/views/clofi.tsx b/src/file-viewer/views/clofi.tsx index b42ef54..08bb426 100644 --- a/src/file-viewer/views/clofi.tsx +++ b/src/file-viewer/views/clofi.tsx @@ -406,7 +406,7 @@ function ListItem({ noDate?: boolean; }) { const dateTime = file.date; - let shortDate = dateTime < unknownDateWithKnownYear + const shortDate = dateTime < unknownDateWithKnownYear ? ( dateTime < unknownDate ? ( @@ -565,40 +565,7 @@ function RootDirView({ hasCotyledonCookie: boolean; }) { const children = dir.getPublicChildren(); - let readme: MediaFile | null = null; - - const groups = { - // years 2025 and onwards - years: [] as MediaFile[], - // named categories - categories: [] as MediaFile[], - // years 2017 to 2024 - cotyledon: [] as MediaFile[], - }; - const colorMap = { - years: "#a2ff91", - categories: "#9c91ff", - cotyledon: "#ff91ca", - }; - for (const child of children) { - const basename = child.basename; - if (basename === readmeFile) { - readme = child; - continue; - } - - const year = basename.match(/^(\d{4})/); - if (year) { - const n = parseInt(year[1]); - if (n >= 2025) { - groups.years.push(child); - } else { - groups.cotyledon.push(child); - } - } else { - groups.categories.push(child); - } - } + const { readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie); if (readme && isLast) activeFilename ||= readmeFile; @@ -613,22 +580,10 @@ function RootDirView({ /> )} - {Object.entries(groups).map(([key, files]) => { - if (key === "cotyledon" && !hasCotyledonCookie) { - return null; - } - if (key === "years" || key === "cotyledon") { - files.sort((a, b) => { - return b.basename.localeCompare(a.basename); - }); - } else { - files.sort((a, b) => { - return a.basename.localeCompare(b.basename); - }); - } + {sections.map(({ key, titleColor, files }) => { return (
-

+

{key}

    @@ -1001,3 +956,4 @@ import { } from "@/file-viewer/format.ts"; import { ForEveryone } from "@/file-viewer/cotyledon.tsx"; import * as mime from "#sitegen/mime"; +import * as sort from "../sort.ts"; diff --git a/src/file-viewer/views/lofi.css b/src/file-viewer/views/lofi.css new file mode 100644 index 0000000..70165f3 --- /dev/null +++ b/src/file-viewer/views/lofi.css @@ -0,0 +1,43 @@ +body { + margin: 0; + padding: 0; +} +#lofi { + padding: 32px; +} +h1 { + margin-top: 0; + font-size: 3em; + color: var(--primary); + font-family: monospace; +} +ul, li { + margin: 0; + padding: 0; + list-style-type: none; +} +ul { + padding-right: 4em; +} +li a { + display: block; + color: white; + line-height: 2em; + padding: 0 1em; + border-radius: 4px; +} +li a:hover { + background-color: rgba(255,255,255,0.2); + font-weight: bold; + text-decoration: none!important; +} +.dir a { + color: #99eeFF +} +.ext { + opacity: 0.5; +} +.meta { + margin-left: 1em; + opacity: 0.75; +} diff --git a/src/file-viewer/views/lofi.marko b/src/file-viewer/views/lofi.marko new file mode 100644 index 0000000..1a1648e --- /dev/null +++ b/src/file-viewer/views/lofi.marko @@ -0,0 +1,71 @@ +import "./lofi.css"; +export interface Input { + file: MediaFile; + hasCotyledonCookie: boolean; +} +export { meta, theme } from "./clofi.tsx"; + + + + + + + + + + 0 + ? formatDuration(file.duration!) + : null + )/> +
  • + + ${formatDate(file.date)}${" "} + ${file.basenameWithoutExt}${file.extension}${dir ? '/' : ''} + (${meta}) + +
  • +
    + +

    + clo's files + ${fullPath} +

    + + + +
    + +

    ${key} +
      + +
    + + +





    +

    + would you like to + dive deeper? +

    + + + + + +

+ +import * as path from "node:path"; +import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts"; +import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts"; +import * as sort from "@/file-viewer/sort.ts"; diff --git a/src/file-viewer/views/lofideon.marko b/src/file-viewer/views/lofideon.marko new file mode 100644 index 0000000..ca2f297 --- /dev/null +++ b/src/file-viewer/views/lofideon.marko @@ -0,0 +1,22 @@ +export interface Input { + stage: number +} +export const meta = { title: 'C O T Y L E D O N' }; +export const theme = { + bg: '#ff00ff', + fg: '#000000', +}; + +

co
ty
le
don

+ + + +

+this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts. +

+by entering our museum, you agree not to use your camera. flash off isn't enough; the bits and bytes are alergic even to a camera's sensor +

+(in english: please do not store downloads after you're done viewing them) +

+ + diff --git a/tsconfig.json b/tsconfig.json index fb86fb8..ae3139f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "allowImportingTsExtensions": true, "baseUrl": ".", "incremental": true, - "jsx": "react-jsx", + "jsx": "react-jsxdev", "jsxImportSource": "#ssr", "lib": ["dom", "esnext", "esnext.iterator"], "module": "nodenext",