authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-06-15 13:11:21-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:45-07:00
logfa86e0da8bb2677603cb5bcda64b565a362359a6
treede1e68df47cd8f90e57210af655dcb854bf7fcab
parentdad822b38eaec73a2696f012cd4e8a4117dd2e87
signature Commit is signed but in an unrecognized format.

finish q+a


20 files changed, 259 insertions(+), 68 deletions(-)

framework/bundle.ts+43-27
......@@ -23,7 +23,8 @@ export async function bundleClientJavaScript(
2323 }
2424
2525 const clientPlugins: esbuild.Plugin[] = [
26 // There are currently no plugins needed by 'paperclover.net'
26 projectRelativeResolution(),
27 markoViaBuildCache(incr),
2728 ];
2829
2930 const bundle = await esbuild.build({
......@@ -42,6 +43,9 @@ export async function bundleClientJavaScript(
4243 jsx: "automatic",
4344 jsxImportSource: "#ssr",
4445 jsxDev: dev,
46 define: {
47 "ASSERT": "console.assert",
48 },
4549 });
4650 if (bundle.errors.length || bundle.warnings.length) {
4751 throw new AggregateError(
......@@ -50,11 +54,15 @@ export async function bundleClientJavaScript(
5054 );
5155 }
5256 const publicScriptRoutes = extraPublicScripts.map((file) =>
53 path.basename(file).replace(/\.client\.[tj]sx?/, "")
57 "/js/" +
58 path.relative(hot.projectSrc, file).replaceAll("\\", "/").replace(
59 /\.client\.[tj]sx?/,
60 ".js",
61 )
5462 );
55 const { metafile } = bundle;
63 const { metafile, outputFiles } = bundle;
5664 const promises: Promise<void>[] = [];
57 for (const file of bundle.outputFiles) {
65 for (const file of outputFiles) {
5866 const { text } = file;
5967 let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
6068 const { inputs } = UNWRAP(metafile.outputs["out!" + route]);
......@@ -63,8 +71,8 @@ export async function bundleClientJavaScript(
6371 // Register non-chunks as script entries.
6472 const chunk = route.startsWith("/js/c.");
6573 if (!chunk) {
66 const key = hot.getScriptId(path.resolve(sources[0]));
67 route = "/js/" + key + ".js";
74 const key = hot.getScriptId(path.resolve(sources[sources.length - 1]));
75 route = "/js/" + key.replace(/\.client\.tsx?/, ".js");
6876 incr.put({
6977 sources,
7078 kind: "script",
......@@ -123,27 +131,7 @@ export async function bundleServerJavaScript(
123131 "$views": viewSource,
124132 }),
125133 projectRelativeResolution(),
126 {
127 name: "marko via build cache",
128 setup(b) {
129 b.onLoad(
130 { filter: /\.marko$/ },
131 async ({ path: file }) => {
132 const key = path.relative(hot.projectRoot, file)
133 .replaceAll("\\", "/");
134 const cacheEntry = incr.out.serverMarko.get(key);
135 if (!cacheEntry) {
136 throw new Error("Marko file not in cache: " + file);
137 }
138 return ({
139 loader: "ts",
140 contents: cacheEntry.src,
141 resolveDir: path.dirname(file),
142 });
143 },
144 );
145 },
146 },
134 markoViaBuildCache(incr),
147135 {
148136 name: "replace client references",
149137 setup(b) {
......@@ -168,6 +156,9 @@ export async function bundleServerJavaScript(
168156 },
169157 },
170158 ];
159 const pkg = await fs.readJson("package.json") as {
160 dependencies: Record<string, string>;
161 };
171162 const { metafile, outputFiles } = await esbuild.build({
172163 bundle: true,
173164 chunkNames: "c.[hash]",
......@@ -186,6 +177,8 @@ export async function bundleServerJavaScript(
186177 jsx: "automatic",
187178 jsxImportSource: "#ssr",
188179 jsxDev: false,
180 external: Object.keys(pkg.dependencies)
181 .filter((x) => !x.startsWith("@paperclover")),
189182 });
190183
191184 const files: Record<string, Buffer> = {};
......@@ -279,6 +272,29 @@ export async function finalizeServerJavaScript(
279272 });
280273}
281274
275function markoViaBuildCache(incr: Incremental): esbuild.Plugin {
276 return {
277 name: "marko via build cache",
278 setup(b) {
279 b.onLoad(
280 { filter: /\.marko$/ },
281 async ({ path: file }) => {
282 const key = path.relative(hot.projectRoot, file)
283 .replaceAll("\\", "/");
284 const cacheEntry = incr.out.serverMarko.get(key);
285 if (!cacheEntry) {
286 throw new Error("Marko file not in cache: " + file);
287 }
288 return ({
289 loader: "ts",
290 contents: cacheEntry.src,
291 resolveDir: path.dirname(file),
292 });
293 },
294 );
295 },
296 };
297}
282298import * as esbuild from "esbuild";
283299import * as path from "node:path";
284300import process from "node:process";
framework/engine/marko-runtime.ts+1-3
......@@ -52,7 +52,6 @@ export const dynamicTag = (
5252 if (typeof tag === "function") {
5353 clover: {
5454 const unwrapped = (tag as any).unwrapped;
55 console.log({ tag, unwrapped });
5655 if (unwrapped) {
5756 tag = unwrapped;
5857 break clover;
......@@ -130,7 +129,7 @@ export function escapeXML(input: unknown) {
130129 ) {
131130 throw new Error(
132131 `Unexpected object in template placeholder: '` +
133 util.inspect({ name: "clover" }) + "'. " +
132 engine.inspect({ name: "clover" }) + "'. " +
134133 `To emit a literal '[object Object]', use \${String(value)}`,
135134 );
136135 }
......@@ -145,4 +144,3 @@ import * as engine from "./ssr.ts";
145144import type { ServerRenderer } from "marko/html/template";
146145import { type Accessor } from "marko/common/types";
147146import * as marko from "#marko/html";
148import * as util from "node:util";
framework/generate.ts+9-6
......@@ -8,9 +8,9 @@ export function main() {
88 successText,
99 failureText: () => "sitegen FAIL",
1010 }, async (spinner) => {
11 const incr = Incremental.fromDisk();
12 await incr.statAllFiles();
13 // const incr = new Incremental();
11 // const incr = Incremental.fromDisk();
12 // await incr.statAllFiles();
13 const incr = new Incremental();
1414 const result = await sitegen(spinner, incr);
1515 incr.toDisk(); // Allows picking up this state again
1616 return result;
......@@ -122,7 +122,6 @@ export async function sitegen(
122122 }
123123 }
124124 }
125 scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
126125 const globalCssPath = join("global.css");
127126
128127 // TODO: make sure that `static` and `pages` does not overlap
......@@ -311,8 +310,12 @@ export async function sitegen(
311310 maxJobs: 2,
312311 });
313312 viewQueue.addMany(neededViews);
314 await pageQueue.done({ method: "stop" });
315 await viewQueue.done({ method: "stop" });
313 const pageAndViews = [
314 pageQueue.done({ method: "stop" }),
315 viewQueue.done({ method: "stop" }),
316 ];
317 await Promise.allSettled(pageAndViews);
318 await Promise.all(pageAndViews);
316319 status.format = spinnerFormat;
317320
318321 // -- bundle server javascript (backend and views) --
framework/hot.ts+5-1
......@@ -63,7 +63,7 @@ Module.prototype._compile = function (
6363 if (shouldTrackPath(filename)) {
6464 const cssImportsMaybe: string[] = [];
6565 const imports: string[] = [];
66 for (const { filename: file } of this.children) {
66 for (const { filename: file, cloverClientRefs } of this.children) {
6767 if (file.endsWith(".css")) cssImportsMaybe.push(file);
6868 else {
6969 const child = fileStats.get(file);
......@@ -71,6 +71,10 @@ Module.prototype._compile = function (
7171 const { cssImportsRecursive } = child;
7272 if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive);
7373 imports.push(file);
74 if (cloverClientRefs && cloverClientRefs.length > 0) {
75 (this.cloverClientRefs ??= [])
76 .push(...cloverClientRefs);
77 }
7478 }
7579 }
7680 fileStats.set(filename, {
framework/incremental.ts+1-1
......@@ -148,7 +148,7 @@ export class Incremental {
148148 for (const key of map.keys()) {
149149 if (!this.round.referenced.has(`${kind}\0${key}`)) {
150150 unreferenced.push({ kind: kind as ArtifactKind, key });
151 this.out[kind as ArtifactKind].delete(key);
151 // this.out[kind as ArtifactKind].delete(key);
152152 }
153153 }
154154 }
framework/lib/assets.ts-2
......@@ -11,7 +11,6 @@ export async function reload() {
1111 fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),
1212 fs.readFile(path.join(import.meta.dirname, "static.blob")),
1313 ]);
14 console.log("new buffer loaded");
1514 assets = {
1615 map: JSON.parse(map),
1716 buf,
......@@ -110,7 +109,6 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
110109}
111110
112111process.on("message", (msg: any) => {
113 console.log({ msg });
114112 if (msg?.type === "clover.assets.reload") reload();
115113});
116114
framework/watch.ts+2-2
......@@ -58,8 +58,8 @@ export async function main() {
5858 successText: generate.successText,
5959 failureText: () => "sitegen FAIL",
6060 }, async (spinner) => {
61 console.log("---");
62 console.log(
61 console.info("---");
62 console.info(
6363 "Updated" +
6464 (changed.length === 1
6565 ? " " + changed[0]
package-lock.json+152
......@@ -8,6 +8,8 @@
88 "@hono/node-server": "^1.14.3",
99 "@mdx-js/mdx": "^3.1.0",
1010 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
11 "codemirror": "^6.0.1",
12 "devalue": "^5.1.1",
1113 "esbuild": "^0.25.5",
1214 "hls.js": "^1.6.5",
1315 "hono": "^4.7.11",
......@@ -404,6 +406,87 @@
404406 "node": ">=6.9.0"
405407 }
406408 },
409 "node_modules/@codemirror/autocomplete": {
410 "version": "6.18.6",
411 "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
412 "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
413 "license": "MIT",
414 "dependencies": {
415 "@codemirror/language": "^6.0.0",
416 "@codemirror/state": "^6.0.0",
417 "@codemirror/view": "^6.17.0",
418 "@lezer/common": "^1.0.0"
419 }
420 },
421 "node_modules/@codemirror/commands": {
422 "version": "6.8.1",
423 "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
424 "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
425 "license": "MIT",
426 "dependencies": {
427 "@codemirror/language": "^6.0.0",
428 "@codemirror/state": "^6.4.0",
429 "@codemirror/view": "^6.27.0",
430 "@lezer/common": "^1.1.0"
431 }
432 },
433 "node_modules/@codemirror/language": {
434 "version": "6.11.1",
435 "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
436 "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
437 "license": "MIT",
438 "dependencies": {
439 "@codemirror/state": "^6.0.0",
440 "@codemirror/view": "^6.23.0",
441 "@lezer/common": "^1.1.0",
442 "@lezer/highlight": "^1.0.0",
443 "@lezer/lr": "^1.0.0",
444 "style-mod": "^4.0.0"
445 }
446 },
447 "node_modules/@codemirror/lint": {
448 "version": "6.8.5",
449 "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
450 "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
451 "license": "MIT",
452 "dependencies": {
453 "@codemirror/state": "^6.0.0",
454 "@codemirror/view": "^6.35.0",
455 "crelt": "^1.0.5"
456 }
457 },
458 "node_modules/@codemirror/search": {
459 "version": "6.5.11",
460 "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
461 "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
462 "license": "MIT",
463 "dependencies": {
464 "@codemirror/state": "^6.0.0",
465 "@codemirror/view": "^6.0.0",
466 "crelt": "^1.0.5"
467 }
468 },
469 "node_modules/@codemirror/state": {
470 "version": "6.5.2",
471 "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
472 "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
473 "license": "MIT",
474 "dependencies": {
475 "@marijn/find-cluster-break": "^1.0.0"
476 }
477 },
478 "node_modules/@codemirror/view": {
479 "version": "6.37.2",
480 "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
481 "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
482 "license": "MIT",
483 "dependencies": {
484 "@codemirror/state": "^6.5.0",
485 "crelt": "^1.0.6",
486 "style-mod": "^4.1.0",
487 "w3c-keyname": "^2.2.4"
488 }
489 },
407490 "node_modules/@esbuild/aix-ppc64": {
408491 "version": "0.25.5",
409492 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
......@@ -864,6 +947,30 @@
864947 "@jridgewell/sourcemap-codec": "^1.4.14"
865948 }
866949 },
950 "node_modules/@lezer/common": {
951 "version": "1.2.3",
952 "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
953 "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
954 "license": "MIT"
955 },
956 "node_modules/@lezer/highlight": {
957 "version": "1.2.1",
958 "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
959 "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
960 "license": "MIT",
961 "dependencies": {
962 "@lezer/common": "^1.0.0"
963 }
964 },
965 "node_modules/@lezer/lr": {
966 "version": "1.4.2",
967 "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
968 "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
969 "license": "MIT",
970 "dependencies": {
971 "@lezer/common": "^1.0.0"
972 }
973 },
867974 "node_modules/@luxass/strip-json-comments": {
868975 "version": "1.4.0",
869976 "resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz",
......@@ -873,6 +980,12 @@
873980 "node": ">=18"
874981 }
875982 },
983 "node_modules/@marijn/find-cluster-break": {
984 "version": "1.0.2",
985 "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
986 "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
987 "license": "MIT"
988 },
876989 "node_modules/@marko/compiler": {
877990 "version": "5.39.21",
878991 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz",
......@@ -1458,6 +1571,21 @@
14581571 "node": ">=8"
14591572 }
14601573 },
1574 "node_modules/codemirror": {
1575 "version": "6.0.1",
1576 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
1577 "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
1578 "license": "MIT",
1579 "dependencies": {
1580 "@codemirror/autocomplete": "^6.0.0",
1581 "@codemirror/commands": "^6.0.0",
1582 "@codemirror/language": "^6.0.0",
1583 "@codemirror/lint": "^6.0.0",
1584 "@codemirror/search": "^6.0.0",
1585 "@codemirror/state": "^6.0.0",
1586 "@codemirror/view": "^6.0.0"
1587 }
1588 },
14611589 "node_modules/collapse-white-space": {
14621590 "version": "2.1.0",
14631591 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
......@@ -1537,6 +1665,12 @@
15371665 }
15381666 }
15391667 },
1668 "node_modules/crelt": {
1669 "version": "1.0.6",
1670 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
1671 "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
1672 "license": "MIT"
1673 },
15401674 "node_modules/csstype": {
15411675 "version": "3.1.3",
15421676 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
......@@ -1605,6 +1739,12 @@
16051739 "node": ">=6"
16061740 }
16071741 },
1742 "node_modules/devalue": {
1743 "version": "5.1.1",
1744 "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
1745 "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
1746 "license": "MIT"
1747 },
16081748 "node_modules/devlop": {
16091749 "version": "1.1.0",
16101750 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
......@@ -3748,6 +3888,12 @@
37483888 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
37493889 }
37503890 },
3891 "node_modules/style-mod": {
3892 "version": "4.1.2",
3893 "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
3894 "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
3895 "license": "MIT"
3896 },
37513897 "node_modules/style-to-js": {
37523898 "version": "1.1.16",
37533899 "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
......@@ -4020,6 +4166,12 @@
40204166 "url": "https://opencollective.com/unified"
40214167 }
40224168 },
4169 "node_modules/w3c-keyname": {
4170 "version": "2.2.8",
4171 "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
4172 "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
4173 "license": "MIT"
4174 },
40234175 "node_modules/wrap-ansi": {
40244176 "version": "7.0.0",
40254177 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
package.json+2
......@@ -4,6 +4,8 @@
44 "@hono/node-server": "^1.14.3",
55 "@mdx-js/mdx": "^3.1.0",
66 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
7 "codemirror": "^6.0.1",
8 "devalue": "^5.1.1",
79 "esbuild": "^0.25.5",
810 "hls.js": "^1.6.5",
911 "hono": "^4.7.11",
src/q+a/backend.ts+4-9
......@@ -127,9 +127,9 @@ app.get("/q+a/:id", async (c, next) => {
127127 const question = Question.getByDate(timestamp);
128128 if (!question) return next();
129129
130 // if (image) {
131 // return getQuestionImage(question, c.req.method === "HEAD");
132 // }
130 if (image) {
131 return getQuestionImage(question, c.req.method === "HEAD");
132 }
133133 return renderView(c, "q+a/permalink", { question });
134134});
135135
......@@ -193,11 +193,6 @@ app.get("/q+a/things/random", async (c) => {
193193 c.res = await renderView(c, "q+a/things-random", {});
194194});
195195
196// 404
197app.get("/q+a/*", async (c) => {
198 return serveAsset(c, "/q+a/404", 404);
199});
200
201196async function questionFailure(
202197 c: Context,
203198 status: ContentfulStatusCode,
......@@ -229,5 +224,5 @@ import {
229224} from "./models/PendingQuestion.ts";
230225import { Question, QuestionType } from "./models/Question.ts";
231226import { renderView } from "#sitegen/view";
232// import { getQuestionImage } from "./question_image";
227import { getQuestionImage } from "./image.tsx";
233228import { formatQuestionId, questionIdToTimestamp } from "./format.ts";
src/q+a/clover-markdown.tsx+1-1
......@@ -211,7 +211,7 @@ function renderNode(node: string | ASTNode | ASTNode[]): any {
211211 if (typeof node === "string") {
212212 return node;
213213 } else if (Array.isArray(node)) {
214 return node.flatMap((item) => renderNode(item));
214 return node.map((item) => renderNode(item));
215215 } else if (node.type === "text") {
216216 return node.content;
217217 } else {
src/q+a/header.client.ts+8
......@@ -1,4 +1,12 @@
11const h1 = document.querySelector("h1")!;
2const key = "net.paperclover.q+a.header";
3const state = localStorage?.getItem(key);
4if (state === "detrevni ton") h1.classList.toggle("invert");
25h1.addEventListener("click", () => {
36 h1.classList.toggle("invert");
7 localStorage?.setItem(
8 key,
9 (localStorage.getItem?.(key) ?? "not inverted")
10 .split("").reverse().join(""),
11 );
412});
src/q+a/image.tsx+1-2
......@@ -5,14 +5,13 @@ const cacheImageDir = path.resolve(".clover/question_images");
55const getBrowser = RefCountedExpirable(
66 () =>
77 puppeteer.launch({
8 // headless: false,
98 args: ["--no-sandbox", "--disable-setuid-sandbox"],
109 }),
1110 (b) => b.close(),
1211);
1312
1413export async function renderQuestionImage(question: Question) {
15 const html = await renderViewToString("q+a/embed-image", { question });
14 const html = await renderViewToString("q+a/image-embed", { question });
1615
1716 // this browser session will be reused if multiple images are generated
1817 // either at the same time or within a 5-minute time span. the dispose
src/q+a/pages/admin.q+a.marko+3
......@@ -6,6 +6,9 @@ export const meta: Metadata = {
66 title: "paper clover q+a",
77 description: "ask clover a question",
88};
9export const regenerate = {
10 manual: true,
11};
912
1013<const/inboxSize = PendingQuestion.getAll().length />
1114<if=(inboxSize > 0)>
src/q+a/pages/q+a.404.marko+1
......@@ -4,6 +4,7 @@ export const meta: Metadata = {
44};
55
66<h2>sound the alarms</h2>
7<p>this page doesn't exist</p>
78<p>
89 <a href="/q+a">return to the questions list</a>
910</p>
src/q+a/pages/q+a.marko+4-1
......@@ -1,4 +1,7 @@
11export * as layout from "../layout.tsx";
2export const regenerate = {
3 manual: true,
4};
25
36export interface Input {
47 admin?: boolean;
......@@ -11,7 +14,7 @@ export const meta: Metadata = {
1114<const/{ admin = false } = input />
1215<const/questions = [...Question.getAll()] />
1316
14<if=!admin>
17<if=true>
1518 <question-form />
1619</>
1720<for|question| of=questions>
src/q+a/scripts/editor.client.tsx+6-5
......@@ -1,9 +1,8 @@
1import { basicSetup, EditorState, EditorView } from "codemirror";
1import { EditorState } from "@codemirror/state";
2import { basicSetup, EditorView } from "codemirror";
23import { ssrSync } from "#ssr";
3// @ts-ignore
4import type { ScriptPayload } from "../views/editor.marko";
5// @ts-ignore
6import QuestionRender from "@/q+a/components/Question.marko";
4import { ScriptPayload } from "@/q+a/view/editor.marko";
5import QuestionRender from "@/q+a/tags/question.marko";
76
87declare const payload: ScriptPayload;
98const date = new Date(payload.date);
......@@ -21,9 +20,11 @@ function updatePreview(text: string) {
2120 type: payload.type,
2221 date,
2322 }}
23 editor
2424 />,
2525 ).text;
2626}
27updatePreview(payload.text);
2728
2829const startState = EditorState.create({
2930 doc: payload.text,
src/q+a/tags/question.marko+6-2
......@@ -2,12 +2,13 @@
22export interface Input {
33 question: Question;
44 admin?: boolean;
5 editor?: boolean;
56}
67
78// 2024-12-31 05:00:00 EST
89export const transitionDate = 1735639200000;
910
10<const/{ question, admin } = input />
11<const/{ question, admin = false, editor = false } = input />
1112<const/{ id, date } = question/>
1213
1314<${"e-"}
......@@ -30,8 +31,11 @@ export const transitionDate = 1735639200000;
3031</>
3132
3233// this singleton script will make all the '<time>' tags clickable.
33client import "./clickable-links.client.ts";
34<if=!editor>
35 client import "./clickable-links.client.ts";
36</>
3437
3538import type { Question } from "@/q+a/models/Question.ts";
3639import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
3740import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
41import { addScript as Script } from "#sitegen"
src/q+a/views/editor.marko+4-2
......@@ -56,10 +56,12 @@ export interface ScriptPayload {
5656 </div>
5757 <div#editor />
5858 <main#preview>
59 <question ...{question} />
59 <question editor ...{ question } />
6060 </main>
6161</div>
62<html-script src="/js/edit_frontend.js" type="module" />
62<html-script>self.payload=${devalue.uneval(payload)}</html-script>
63<html-script src="/js/q+a/scripts/editor.js" type="module" />
6364
65import * as devalue from 'devalue';
6466import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";
6567import { Question, QuestionType } from "@/q+a/models/Question.ts";
src/q+a/views/permalink.marko+6-4
......@@ -1,23 +1,25 @@
1export * as layout from "@/q+a/layout.tsx";
12export interface Input {
23 question: Question;
34}
45
5server export function meta({ context: { req }, question }) {
6 const isDiscord = req.get("user-agent")
6server export function meta({ context, question }) {
7 const isDiscord = context.get("user-agent")
78 ?.toLowerCase()
89 .includes("discordbot");
910 if (question.type === QuestionType.normal) {
1011 return {
1112 title: "question permalink",
1213 openGraph: {
13 images: [{ url: `https://paperclover.net/q+a/${q.id}.png` }],
14 images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
1415 },
1516 twitter: { card: "summary_large_image" },
1617 themeColor: isDiscord
17 ? q.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
18 ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
1819 : undefined,
1920 };
2021 }
22 return { title: 'question permalink' };
2123}
2224
2325<const/{ question }=input/>