authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-27 20:40:19-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-27 22:30:26-07:00
logae4477792c3ad86f158accb18e892e77aa48cf00
treec09ed54383545520ffeafa2d7a511be14c528883
parentb01c9ea08ff53136b25ad5c35f26fcfa1917aaf0
signature Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

chore: remove remaining JSX in the questions page

drops the ability to do `.client.tsx`

18 files changed, 429 insertions(+), 461 deletions(-)

framework/generate.ts+3-3
......@@ -184,7 +184,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) {
184184 list: pages,
185185 prefix: section.base,
186186 include: [".tsx", ".marko", ".mdo"],
187 exclude: [".client.ts", ".client.tsx"],
187 exclude: [".client.ts"],
188188 page: true,
189189 },
190190 {
......@@ -198,7 +198,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) {
198198 dir: sectionPath("scripts"),
199199 list: scripts,
200200 prefix: rootPrefix,
201 include: [".client.ts", ".client.tsx"],
201 include: [".client.ts"],
202202 page: false,
203203 },
204204 {
......@@ -206,7 +206,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) {
206206 list: views,
207207 prefix: rootPrefix,
208208 include: [".tsx", ".marko", ".mdo"],
209 exclude: [".client.ts", ".client.tsx"],
209 exclude: [".client.ts"],
210210 page: false,
211211 },
212212 ];
framework/hot.ts+2-2
......@@ -144,8 +144,8 @@ export function loadEsbuildCode(
144144
145145export function resolveClientRef(sourcePath: string, ref: string) {
146146 const filePath = resolveFrom(sourcePath, ref);
147 if (!filePath.endsWith(".client.ts") && !filePath.endsWith(".client.tsx")) {
148 throw new Error("addScript must be a .client.ts or .client.tsx");
147 if (!filePath.endsWith(".client.ts")) {
148 throw new Error("addScript must take a .client.ts");
149149 }
150150 return path.relative(projectSrc, filePath);
151151}
package-lock.json+129-136
......@@ -11,7 +11,6 @@
1111 "@marko/compiler": "5.39.62",
1212 "@types/node": "^24.12.0",
1313 "dashjs": "5.0.3",
14 "devalue": "5.6.4",
1514 "esbuild": "0.25.5",
1615 "harfbuzzjs": "0.10.1",
1716 "hls.js": "1.6.5",
......@@ -22,7 +21,7 @@
2221 "source-map-support": "0.5.21",
2322 "typescript": "^5.9.3",
2423 "unique-names-generator": "4.7.1",
25 "vitest": "^4.0.18",
24 "vitest": "4.1.2",
2625 "vscode-oniguruma": "2.0.1",
2726 "vscode-textmate": "9.2.0",
2827 "wawoff2": "2.0.1"
......@@ -670,18 +669,18 @@
670669 }
671670 },
672671 "node_modules/@oxc-project/types": {
673 "version": "0.120.0",
674 "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz",
675 "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==",
672 "version": "0.122.0",
673 "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
674 "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
676675 "license": "MIT",
677676 "funding": {
678677 "url": "https://github.com/sponsors/Boshen"
679678 }
680679 },
681680 "node_modules/@rolldown/binding-android-arm64": {
682 "version": "1.0.0-rc.10",
683 "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz",
684 "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==",
681 "version": "1.0.0-rc.12",
682 "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
683 "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
685684 "cpu": [
686685 "arm64"
687686 ],
......@@ -695,9 +694,9 @@
695694 }
696695 },
697696 "node_modules/@rolldown/binding-darwin-arm64": {
698 "version": "1.0.0-rc.10",
699 "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz",
700 "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==",
697 "version": "1.0.0-rc.12",
698 "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
699 "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
701700 "cpu": [
702701 "arm64"
703702 ],
......@@ -711,9 +710,9 @@
711710 }
712711 },
713712 "node_modules/@rolldown/binding-darwin-x64": {
714 "version": "1.0.0-rc.10",
715 "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz",
716 "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==",
713 "version": "1.0.0-rc.12",
714 "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
715 "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
717716 "cpu": [
718717 "x64"
719718 ],
......@@ -727,9 +726,9 @@
727726 }
728727 },
729728 "node_modules/@rolldown/binding-freebsd-x64": {
730 "version": "1.0.0-rc.10",
731 "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz",
732 "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==",
729 "version": "1.0.0-rc.12",
730 "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
731 "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
733732 "cpu": [
734733 "x64"
735734 ],
......@@ -743,9 +742,9 @@
743742 }
744743 },
745744 "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
746 "version": "1.0.0-rc.10",
747 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz",
748 "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==",
745 "version": "1.0.0-rc.12",
746 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
747 "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
749748 "cpu": [
750749 "arm"
751750 ],
......@@ -759,9 +758,9 @@
759758 }
760759 },
761760 "node_modules/@rolldown/binding-linux-arm64-gnu": {
762 "version": "1.0.0-rc.10",
763 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz",
764 "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==",
761 "version": "1.0.0-rc.12",
762 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
763 "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
765764 "cpu": [
766765 "arm64"
767766 ],
......@@ -775,9 +774,9 @@
775774 }
776775 },
777776 "node_modules/@rolldown/binding-linux-arm64-musl": {
778 "version": "1.0.0-rc.10",
779 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz",
780 "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==",
777 "version": "1.0.0-rc.12",
778 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
779 "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
781780 "cpu": [
782781 "arm64"
783782 ],
......@@ -791,9 +790,9 @@
791790 }
792791 },
793792 "node_modules/@rolldown/binding-linux-ppc64-gnu": {
794 "version": "1.0.0-rc.10",
795 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz",
796 "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==",
793 "version": "1.0.0-rc.12",
794 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
795 "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
797796 "cpu": [
798797 "ppc64"
799798 ],
......@@ -807,9 +806,9 @@
807806 }
808807 },
809808 "node_modules/@rolldown/binding-linux-s390x-gnu": {
810 "version": "1.0.0-rc.10",
811 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz",
812 "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==",
809 "version": "1.0.0-rc.12",
810 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
811 "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
813812 "cpu": [
814813 "s390x"
815814 ],
......@@ -823,9 +822,9 @@
823822 }
824823 },
825824 "node_modules/@rolldown/binding-linux-x64-gnu": {
826 "version": "1.0.0-rc.10",
827 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz",
828 "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==",
825 "version": "1.0.0-rc.12",
826 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
827 "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
829828 "cpu": [
830829 "x64"
831830 ],
......@@ -839,9 +838,9 @@
839838 }
840839 },
841840 "node_modules/@rolldown/binding-linux-x64-musl": {
842 "version": "1.0.0-rc.10",
843 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz",
844 "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==",
841 "version": "1.0.0-rc.12",
842 "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
843 "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
845844 "cpu": [
846845 "x64"
847846 ],
......@@ -855,9 +854,9 @@
855854 }
856855 },
857856 "node_modules/@rolldown/binding-openharmony-arm64": {
858 "version": "1.0.0-rc.10",
859 "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz",
860 "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==",
857 "version": "1.0.0-rc.12",
858 "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
859 "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
861860 "cpu": [
862861 "arm64"
863862 ],
......@@ -871,9 +870,9 @@
871870 }
872871 },
873872 "node_modules/@rolldown/binding-wasm32-wasi": {
874 "version": "1.0.0-rc.10",
875 "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz",
876 "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==",
873 "version": "1.0.0-rc.12",
874 "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
875 "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
877876 "cpu": [
878877 "wasm32"
879878 ],
......@@ -887,9 +886,9 @@
887886 }
888887 },
889888 "node_modules/@rolldown/binding-win32-arm64-msvc": {
890 "version": "1.0.0-rc.10",
891 "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz",
892 "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==",
889 "version": "1.0.0-rc.12",
890 "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
891 "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
893892 "cpu": [
894893 "arm64"
895894 ],
......@@ -903,9 +902,9 @@
903902 }
904903 },
905904 "node_modules/@rolldown/binding-win32-x64-msvc": {
906 "version": "1.0.0-rc.10",
907 "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz",
908 "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==",
905 "version": "1.0.0-rc.12",
906 "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
907 "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
909908 "cpu": [
910909 "x64"
911910 ],
......@@ -919,9 +918,9 @@
919918 }
920919 },
921920 "node_modules/@rolldown/pluginutils": {
922 "version": "1.0.0-rc.10",
923 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz",
924 "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==",
921 "version": "1.0.0-rc.12",
922 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
923 "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
925924 "license": "MIT"
926925 },
927926 "node_modules/@standard-schema/spec": {
......@@ -987,17 +986,17 @@
987986 }
988987 },
989988 "node_modules/@vitest/expect": {
990 "version": "4.1.0",
991 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz",
992 "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==",
989 "version": "4.1.2",
990 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
991 "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==",
993992 "license": "MIT",
994993 "dependencies": {
995994 "@standard-schema/spec": "^1.1.0",
996995 "@types/chai": "^5.2.2",
997 "@vitest/spy": "4.1.0",
998 "@vitest/utils": "4.1.0",
996 "@vitest/spy": "4.1.2",
997 "@vitest/utils": "4.1.2",
999998 "chai": "^6.2.2",
1000 "tinyrainbow": "^3.0.3"
999 "tinyrainbow": "^3.1.0"
10011000 },
10021001 "funding": {
10031002 "url": "https://opencollective.com/vitest"
......@@ -1013,24 +1012,24 @@
10131012 }
10141013 },
10151014 "node_modules/@vitest/pretty-format": {
1016 "version": "4.1.0",
1017 "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz",
1018 "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==",
1015 "version": "4.1.2",
1016 "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
1017 "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
10191018 "license": "MIT",
10201019 "dependencies": {
1021 "tinyrainbow": "^3.0.3"
1020 "tinyrainbow": "^3.1.0"
10221021 },
10231022 "funding": {
10241023 "url": "https://opencollective.com/vitest"
10251024 }
10261025 },
10271026 "node_modules/@vitest/runner": {
1028 "version": "4.1.0",
1029 "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz",
1030 "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==",
1027 "version": "4.1.2",
1028 "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz",
1029 "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==",
10311030 "license": "MIT",
10321031 "dependencies": {
1033 "@vitest/utils": "4.1.0",
1032 "@vitest/utils": "4.1.2",
10341033 "pathe": "^2.0.3"
10351034 },
10361035 "funding": {
......@@ -1038,13 +1037,13 @@
10381037 }
10391038 },
10401039 "node_modules/@vitest/snapshot": {
1041 "version": "4.1.0",
1042 "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz",
1043 "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==",
1040 "version": "4.1.2",
1041 "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz",
1042 "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==",
10441043 "license": "MIT",
10451044 "dependencies": {
1046 "@vitest/pretty-format": "4.1.0",
1047 "@vitest/utils": "4.1.0",
1045 "@vitest/pretty-format": "4.1.2",
1046 "@vitest/utils": "4.1.2",
10481047 "magic-string": "^0.30.21",
10491048 "pathe": "^2.0.3"
10501049 },
......@@ -1053,23 +1052,23 @@
10531052 }
10541053 },
10551054 "node_modules/@vitest/spy": {
1056 "version": "4.1.0",
1057 "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz",
1058 "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==",
1055 "version": "4.1.2",
1056 "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
1057 "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
10591058 "license": "MIT",
10601059 "funding": {
10611060 "url": "https://opencollective.com/vitest"
10621061 }
10631062 },
10641063 "node_modules/@vitest/utils": {
1065 "version": "4.1.0",
1066 "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz",
1067 "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==",
1064 "version": "4.1.2",
1065 "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
1066 "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
10681067 "license": "MIT",
10691068 "dependencies": {
1070 "@vitest/pretty-format": "4.1.0",
1069 "@vitest/pretty-format": "4.1.2",
10711070 "convert-source-map": "^2.0.0",
1072 "tinyrainbow": "^3.0.3"
1071 "tinyrainbow": "^3.1.0"
10731072 },
10741073 "funding": {
10751074 "url": "https://opencollective.com/vitest"
......@@ -1257,12 +1256,6 @@
12571256 "node": ">=8"
12581257 }
12591258 },
1260 "node_modules/devalue": {
1261 "version": "5.6.4",
1262 "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
1263 "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
1264 "license": "MIT"
1265 },
12661259 "node_modules/error-stack-parser": {
12671260 "version": "2.1.4",
12681261 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
......@@ -1952,9 +1945,9 @@
19521945 "license": "ISC"
19531946 },
19541947 "node_modules/picomatch": {
1955 "version": "4.0.3",
1956 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1957 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1948 "version": "4.0.4",
1949 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1950 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
19581951 "license": "MIT",
19591952 "peer": true,
19601953 "engines": {
......@@ -2056,13 +2049,13 @@
20562049 }
20572050 },
20582051 "node_modules/rolldown": {
2059 "version": "1.0.0-rc.10",
2060 "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz",
2061 "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==",
2052 "version": "1.0.0-rc.12",
2053 "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
2054 "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
20622055 "license": "MIT",
20632056 "dependencies": {
2064 "@oxc-project/types": "=0.120.0",
2065 "@rolldown/pluginutils": "1.0.0-rc.10"
2057 "@oxc-project/types": "=0.122.0",
2058 "@rolldown/pluginutils": "1.0.0-rc.12"
20662059 },
20672060 "bin": {
20682061 "rolldown": "bin/cli.mjs"
......@@ -2071,21 +2064,21 @@
20712064 "node": "^20.19.0 || >=22.12.0"
20722065 },
20732066 "optionalDependencies": {
2074 "@rolldown/binding-android-arm64": "1.0.0-rc.10",
2075 "@rolldown/binding-darwin-arm64": "1.0.0-rc.10",
2076 "@rolldown/binding-darwin-x64": "1.0.0-rc.10",
2077 "@rolldown/binding-freebsd-x64": "1.0.0-rc.10",
2078 "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10",
2079 "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10",
2080 "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10",
2081 "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10",
2082 "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10",
2083 "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10",
2084 "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10",
2085 "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10",
2086 "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10",
2087 "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10",
2088 "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10"
2067 "@rolldown/binding-android-arm64": "1.0.0-rc.12",
2068 "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
2069 "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
2070 "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
2071 "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
2072 "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
2073 "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
2074 "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
2075 "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
2076 "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
2077 "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
2078 "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
2079 "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
2080 "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
2081 "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
20892082 }
20902083 },
20912084 "node_modules/sax": {
......@@ -2266,18 +2259,18 @@
22662259 }
22672260 },
22682261 "node_modules/vitest": {
2269 "version": "4.1.0",
2270 "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz",
2271 "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==",
2262 "version": "4.1.2",
2263 "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz",
2264 "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
22722265 "license": "MIT",
22732266 "dependencies": {
2274 "@vitest/expect": "4.1.0",
2275 "@vitest/mocker": "4.1.0",
2276 "@vitest/pretty-format": "4.1.0",
2277 "@vitest/runner": "4.1.0",
2278 "@vitest/snapshot": "4.1.0",
2279 "@vitest/spy": "4.1.0",
2280 "@vitest/utils": "4.1.0",
2267 "@vitest/expect": "4.1.2",
2268 "@vitest/mocker": "4.1.2",
2269 "@vitest/pretty-format": "4.1.2",
2270 "@vitest/runner": "4.1.2",
2271 "@vitest/snapshot": "4.1.2",
2272 "@vitest/spy": "4.1.2",
2273 "@vitest/utils": "4.1.2",
22812274 "es-module-lexer": "^2.0.0",
22822275 "expect-type": "^1.3.0",
22832276 "magic-string": "^0.30.21",
......@@ -2288,8 +2281,8 @@
22882281 "tinybench": "^2.9.0",
22892282 "tinyexec": "^1.0.2",
22902283 "tinyglobby": "^0.2.15",
2291 "tinyrainbow": "^3.0.3",
2292 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0",
2284 "tinyrainbow": "^3.1.0",
2285 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
22932286 "why-is-node-running": "^2.3.0"
22942287 },
22952288 "bin": {
......@@ -2305,13 +2298,13 @@
23052298 "@edge-runtime/vm": "*",
23062299 "@opentelemetry/api": "^1.9.0",
23072300 "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
2308 "@vitest/browser-playwright": "4.1.0",
2309 "@vitest/browser-preview": "4.1.0",
2310 "@vitest/browser-webdriverio": "4.1.0",
2311 "@vitest/ui": "4.1.0",
2301 "@vitest/browser-playwright": "4.1.2",
2302 "@vitest/browser-preview": "4.1.2",
2303 "@vitest/browser-webdriverio": "4.1.2",
2304 "@vitest/ui": "4.1.2",
23122305 "happy-dom": "*",
23132306 "jsdom": "*",
2314 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
2307 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
23152308 },
23162309 "peerDependenciesMeta": {
23172310 "@edge-runtime/vm": {
......@@ -2747,12 +2740,12 @@
27472740 }
27482741 },
27492742 "node_modules/vitest/node_modules/@vitest/mocker": {
2750 "version": "4.1.0",
2751 "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz",
2752 "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==",
2743 "version": "4.1.2",
2744 "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
2745 "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==",
27532746 "license": "MIT",
27542747 "dependencies": {
2755 "@vitest/spy": "4.1.0",
2748 "@vitest/spy": "4.1.2",
27562749 "estree-walker": "^3.0.3",
27572750 "magic-string": "^0.30.21"
27582751 },
......@@ -2761,7 +2754,7 @@
27612754 },
27622755 "peerDependencies": {
27632756 "msw": "^2.4.9",
2764 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
2757 "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
27652758 },
27662759 "peerDependenciesMeta": {
27672760 "msw": {
......@@ -2787,16 +2780,16 @@
27872780 }
27882781 },
27892782 "node_modules/vitest/node_modules/vite": {
2790 "version": "8.0.1",
2791 "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz",
2792 "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==",
2783 "version": "8.0.3",
2784 "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
2785 "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
27932786 "license": "MIT",
27942787 "peer": true,
27952788 "dependencies": {
27962789 "lightningcss": "^1.32.0",
2797 "picomatch": "^4.0.3",
2790 "picomatch": "^4.0.4",
27982791 "postcss": "^8.5.8",
2799 "rolldown": "1.0.0-rc.10",
2792 "rolldown": "1.0.0-rc.12",
28002793 "tinyglobby": "^0.2.15"
28012794 },
28022795 "bin": {
package.json+1-2
......@@ -7,7 +7,6 @@
77 "@marko/compiler": "5.39.62",
88 "@types/node": "^24.12.0",
99 "dashjs": "5.0.3",
10 "devalue": "5.6.4",
1110 "esbuild": "0.25.5",
1211 "harfbuzzjs": "0.10.1",
1312 "hls.js": "1.6.5",
......@@ -18,7 +17,7 @@
1817 "source-map-support": "0.5.21",
1918 "typescript": "^5.9.3",
2019 "unique-names-generator": "4.7.1",
21 "vitest": "^4.0.18",
20 "vitest": "4.1.2",
2221 "vscode-oniguruma": "2.0.1",
2322 "vscode-textmate": "9.2.0",
2423 "wawoff2": "2.0.1"
src/blog/drafts/webdev/marko-intro.markodown+1-1
......@@ -330,7 +330,7 @@ client import "./clickable-links.client.ts";
330330
331331import type { Question } from "#src/q+a/models/Question.ts";
332332import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "#src/q+a/format.ts";
333import { QuestionMarkdown } from "#src/q+a/markdown.tsx";
333import { QuestionMarkdown } from "#src/q+a/markdown.ts";
334334```
335335
336336The integration is great, `client import` is quite a magical concept, and I've
src/blog/localization.ts created+34
......@@ -0,0 +1,34 @@
1interface LanguageConfig {
2 title: render.Node;
3 return: render.Node;
4 writtenByClover: string;
5 translatedBy: (_: { author: string }) => render.Node;
6 tableOfContents: string;
7}
8type LanguageMap = Record<string, LanguageConfig> & { en: LanguageConfig };
9
10export const languages: LanguageMap = {
11 en: {
12 title: "English",
13 return: "back to the home page",
14 writtenByClover: `clover caruso`, // "written by clover caruso" in other languages
15 translatedBy: ({ author }) => `translated by ${author}`,
16 tableOfContents: "Contents",
17 },
18 ko: {
19 title: "한국어",
20 return: "홈 페이지로 돌아가기",
21 writtenByClover: `clover caruso 작성`,
22 translatedBy: ({ author }) => `${author} 번역`,
23 tableOfContents: "Contents",
24 },
25 es: {
26 title: "Español",
27 return: "volver a la página principal",
28 writtenByClover: "escrito por clover caruso",
29 translatedBy: ({ author }) => `traducido por ${author}`,
30 tableOfContents: "Indice",
31 },
32};
33
34import * as render from "@clo/lib/render";
src/blog/localization.tsx deleted-34
......@@ -1,34 +0,0 @@
1interface LanguageConfig {
2 title: render.Node;
3 return: render.Node;
4 writtenByClover: string;
5 translatedBy: (_: { author: string }) => render.Node;
6 tableOfContents: string;
7}
8type LanguageMap = Record<string, LanguageConfig> & { en: LanguageConfig };
9
10export const languages: LanguageMap = {
11 en: {
12 title: "English",
13 return: "back to the home page",
14 writtenByClover: `clover caruso`, // "written by clover caruso" in other languages
15 translatedBy: ({ author }) => `translated by ${author}`,
16 tableOfContents: "Contents",
17 },
18 ko: {
19 title: "한국어",
20 return: "홈 페이지로 돌아가기",
21 writtenByClover: `clover caruso 작성`,
22 translatedBy: ({ author }) => `${author} 번역`,
23 tableOfContents: "Contents",
24 },
25 es: {
26 title: "Español",
27 return: "volver a la página principal",
28 writtenByClover: "escrito por clover caruso",
29 translatedBy: ({ author }) => `traducido por ${author}`,
30 tableOfContents: "Indice",
31 },
32};
33
34import * as render from "@clo/lib/render";
src/blog/tags/blog-layout.marko+1-1
......@@ -99,7 +99,7 @@ export const markoOnly = true;
9999
100100</main>
101101
102import * as localization from "../localization.tsx";
102import * as localization from "../localization.ts";
103103import * as markodown from "@clo/markodown";
104104import Heading from "./heading.marko";
105105import CodeBlock from "./code-block.marko";
src/file-viewer/cert.pem deleted-23
......@@ -1,23 +0,0 @@
1-----BEGIN CERTIFICATE-----
2MIIDxTCCAq2gAwIBAgIUBaaOXVkkE+6yarNyvzofETb+WLEwDQYJKoZIhvcNAQEL
3BQAwdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
4MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0aDEhMB8GCSqG
5SIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MB4XDTI1MDQyNzIxNTU0MFoXDTM1
6MDQyNTIxNTU0MFowdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYD
7VQQHDARDaXR5MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0
8aDEhMB8GCSqGSIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MIIBIjANBgkqhkiG
99w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7lLwx8XwsuTeaIxTsHDL+Lx7eblsJ0XylVm
100/iIJS1Mrq6Be9St6vDWK/BWqqAn+MdqzSfLMy8EKazuHKtbTm2vlUIkjw28SoWP
116cRSCLx4hFGbF4tmRO+Bo+/4PpHPnheeolkjJ+CLO87tZ752D9JzjVND+WIj1QO+
12bm+JBIi1TFREPh22/fSZBRpaRgqHcUEhICaiXaufvxQ6eihQfGSe00I7zRzGgnMl
1351xjzkKkXd+r/FwTykd8ScJN25FMVDLsfJR59//geAZXYS25gQ4YL6R8u7ijidlS
14IoDG8N+Fzw7W4yI+y8fIN4W1x/HsjiQ665CuWY3TMYo98OaGwwIDAQABo0kwRzAm
15BgNVHREEHzAdggZ6ZW5pdGiCE25hcy5wYXBlcmNsb3Zlci5uZXQwHQYDVR0OBBYE
16FDXkgNsMYZv1Pr+95RCCk7eHACGOMA0GCSqGSIb3DQEBCwUAA4IBAQB6942odKyD
17TudifxRXbvcVe9LxSd7NimxRZzM5wTgA5KkxQT4CBM2wEPH/7e7Q/8scB9HbH2uP
18f2vixoCM+Z3BWiYHFFk+1pf2myUdiFV2BC9g80txEerRkGLc18V6CdYNJ9wNPkiO
19LW/RzXfEv+sqhaXh8dA46Ruz6SAbmscTMMYW4e9VYR+1p4Sm5UpTxrHzeg21YJKn
20ud8kO1r7RhVgUGzkAzNaIMiBuJqGGdD5yV7Ng5C/DlJ9AAeYu1diM5LkIKjf+/8M
21t/3l4eXS3Lda6+21rDvmfoK4Za6CAhcwgXIpqiRixE2MQNsxZ2XiJBVQHPrh8xYk
22L5fq8KTGFwtd
23-----END CERTIFICATE-----
src/q+a/backend.ts+1-1
......@@ -227,5 +227,5 @@ import { serveAsset } from "#sitegen/assets";
227227import type { PendingQuestionData } from "./models/PendingQuestion.ts";
228228import { Question, QuestionType } from "./models/Question.ts";
229229import * as view from "#sitegen/view";
230import { getQuestionImage } from "./image.tsx";
230import { getQuestionImage } from "./image.ts";
231231import { formatQuestionId, questionIdToTimestamp } from "./format.ts";
src/q+a/image.ts created+79
......@@ -0,0 +1,79 @@
1const port = Number(process.env.PORT ?? 3000);
2const width = 768;
3const cacheImageDir = path.resolve(".clover/question_images");
4
5const getBrowser = RefCountedExpirable(
6 () =>
7 playwright.chromium.launch({
8 args: ["--no-sandbox", "--disable-setuid-sandbox"],
9 }),
10 (b) => b.close(),
11);
12
13export async function renderQuestionImage(question: Question) {
14 using sharedBrowser = await getBrowser();
15 const b = sharedBrowser.value;
16 const ctx = await b.newContext({
17 userAgent: "clover-qa-image",
18 deviceScaleFactor: 1.5,
19 });
20 try {
21 const p = await ctx.newPage();
22 await p.setViewportSize({ width, height: 400 });
23 await p.goto(`http://localhost:${port}/q+a/${question.id}`);
24 try {
25 await p.waitForLoadState("networkidle", { timeout: 500 });
26 } catch {}
27 const height = await p.evaluate(() => {
28 document.body.style.background = "#1a1b1c";
29 return document.querySelector("main")!.getBoundingClientRect().height;
30 });
31 return await p.screenshot({
32 type: "png",
33 clip: { x: 0, y: 0, width, height },
34 });
35 } finally {
36 await ctx.close();
37 }
38}
39
40export async function getQuestionImage(
41 question: Question,
42 headOnly: boolean,
43): Promise<Response> {
44 const hash = crypto
45 .createHash("sha1")
46 .update(question.qmid + question.type + question.text)
47 .digest("hex");
48
49 const headers = {
50 "Content-Type": "image/png",
51 "Cache-Control": "public, max-age=31536000",
52 ETag: `"${hash}"`,
53 "Last-Modified": question.date.toUTCString(),
54 };
55
56 if (headOnly) {
57 return new Response(null, { headers });
58 }
59
60 const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`);
61 let buf: Buffer;
62 try {
63 buf = await fs.readFile(cachedFilePath);
64 } catch (e: any) {
65 if (e.code !== "ENOENT") throw e;
66 buf = await renderQuestionImage(question);
67 fs.writeMkdir(cachedFilePath, buf).catch(() => {});
68 }
69
70 return new Response(buf, { headers });
71}
72
73import * as crypto from "node:crypto";
74import * as fs from "#sitegen/fs";
75import * as path from "node:path";
76import { Question } from "#src/q+a/models/Question.ts";
77import { RefCountedExpirable } from "@clo/lib/async";
78import * as view from "#sitegen/view";
79import * as playwright from "playwright";
src/q+a/image.tsx deleted-79
......@@ -1,79 +0,0 @@
1const port = Number(process.env.PORT ?? 3000);
2const width = 768;
3const cacheImageDir = path.resolve(".clover/question_images");
4
5const getBrowser = RefCountedExpirable(
6 () =>
7 playwright.chromium.launch({
8 args: ["--no-sandbox", "--disable-setuid-sandbox"],
9 }),
10 (b) => b.close(),
11);
12
13export async function renderQuestionImage(question: Question) {
14 using sharedBrowser = await getBrowser();
15 const b = sharedBrowser.value;
16 const ctx = await b.newContext({
17 userAgent: "clover-qa-image",
18 deviceScaleFactor: 1.5,
19 });
20 try {
21 const p = await ctx.newPage();
22 await p.setViewportSize({ width, height: 400 });
23 await p.goto(`http://localhost:${port}/q+a/${question.id}`);
24 try {
25 await p.waitForLoadState("networkidle", { timeout: 500 });
26 } catch {}
27 const height = await p.evaluate(() => {
28 document.body.style.background = "#1a1b1c";
29 return document.querySelector("main")!.getBoundingClientRect().height;
30 });
31 return await p.screenshot({
32 type: "png",
33 clip: { x: 0, y: 0, width, height },
34 });
35 } finally {
36 await ctx.close();
37 }
38}
39
40export async function getQuestionImage(
41 question: Question,
42 headOnly: boolean,
43): Promise<Response> {
44 const hash = crypto
45 .createHash("sha1")
46 .update(question.qmid + question.type + question.text)
47 .digest("hex");
48
49 const headers = {
50 "Content-Type": "image/png",
51 "Cache-Control": "public, max-age=31536000",
52 ETag: `"${hash}"`,
53 "Last-Modified": question.date.toUTCString(),
54 };
55
56 if (headOnly) {
57 return new Response(null, { headers });
58 }
59
60 const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`);
61 let buf: Buffer;
62 try {
63 buf = await fs.readFile(cachedFilePath);
64 } catch (e: any) {
65 if (e.code !== "ENOENT") throw e;
66 buf = await renderQuestionImage(question);
67 fs.writeMkdir(cachedFilePath, buf).catch(() => {});
68 }
69
70 return new Response(buf, { headers });
71}
72
73import * as crypto from "node:crypto";
74import * as fs from "#sitegen/fs";
75import * as path from "node:path";
76import { Question } from "#src/q+a/models/Question.ts";
77import { RefCountedExpirable } from "@clo/lib/async";
78import * as view from "#sitegen/view";
79import * as playwright from "playwright";
src/q+a/markdown.ts created+81
......@@ -0,0 +1,81 @@
1function labelledRedaction(contents: string) {
2 return (
3 `<span class="dead">` +
4 (contents === "name"
5 ? "old name"
6 : contents === "username"
7 ? "old username"
8 : contents === "domain"
9 ? "old domain"
10 : null) +
11 `</span>`
12 );
13}
14
15function questionRef(id: string) {
16 if (id.length === 12) id = id.slice(0, 10);
17 const timestamp = questionIdToTimestamp(id);
18 if (!timestamp) {
19 console.error(`Invalid question ref: ${id}`);
20 return `<span class="question-ref">[Invalid Question Ref: #${
21 render.escapeHtmlContent(id)
22 }]</span>`;
23 }
24 return `<a class="custom question-ref" href="/q+a/${id}">${
25 formatQuestionTimestamp(timestamp)
26 }</a>`;
27}
28
29function artifactRef(id: string) {
30 const artifact = artifactMap[id];
31 if (!artifact) {
32 return `<span class="artifact">[Invalid Artifact Ref: @${
33 render.escapeHtmlContent(id)
34 }]</span>`;
35 }
36 const [title, url, type] = artifact;
37 if (!url) {
38 return `<span>${render.escapeHtmlContent(title)}</span>`;
39 }
40 return `<a class="custom artifact artifact-${type}" href="${url}">` +
41 render.escapeHtmlContent(title) +
42 `</a>`;
43}
44
45/** classic esbuild trick: use an impossible to guess key for string magic */
46const uniqueKey = crypto.randomUUID().replaceAll("-", "");
47
48const renderers = {
49 A: artifactRef,
50 Q: questionRef,
51 R: labelledRedaction,
52} as const;
53
54export function renderMarkdown(source: string) {
55 const tagNames: markodown.CloverQuestionExtensions = {
56 question: "q-",
57 artifactRef: uniqueKey + "_A",
58 questionRef: uniqueKey + "_Q",
59 labelledRedaction: uniqueKey + "_R",
60 };
61 const result = markodown.transform({
62 source,
63 markdownOnly: true,
64 cloverExtensions: tagNames,
65 });
66 if (!result.success) {
67 throw new Error(JSON.stringify(result.errors));
68 }
69 return result.text
70 .replace(
71 new RegExp(`<${uniqueKey}_([AQR])>([^<]*)<\\/${uniqueKey}_[AQR]>`, "g"),
72 (_, type: "A" | "Q" | "R", content: string) => {
73 return renderers[type](content);
74 },
75 );
76}
77
78import * as markodown from "@clo/markodown";
79import * as render from "@clo/lib/render";
80import { artifactMap } from "./artifacts.ts";
81import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts";
src/q+a/markdown.tsx deleted-81
......@@ -1,81 +0,0 @@
1function labelledRedaction(contents: string) {
2 return (
3 <span class="dead">
4 {contents === "name"
5 ? "old name"
6 : contents === "username"
7 ? "old username"
8 : contents === "domain"
9 ? "old domain"
10 : null}
11 </span>
12 );
13}
14
15function questionRef(id: string) {
16 if (id.length === 12) id = id.slice(0, 10);
17 const timestamp = questionIdToTimestamp(id);
18 if (!timestamp) {
19 console.error(`Invalid question ref: ${id}`);
20 return <span class="question-ref">[Invalid Question Ref: #{id}]</span>;
21 }
22 return (
23 <a class="custom question-ref" href={`/q+a/${id}`}>
24 {formatQuestionTimestamp(timestamp)}
25 </a>
26 );
27}
28
29function artifactRef(id: string) {
30 const artifact = artifactMap[id];
31 if (!artifact) {
32 return <span class="artifact">[Invalid Artifact Ref: @{id}]</span>;
33 }
34 const [title, url, type] = artifact;
35 if (!url) {
36 return <span>{title}</span>;
37 }
38 return (
39 <a class={`custom artifact artifact-${type}`} href={url}>
40 {title}
41 </a>
42 );
43}
44
45/** classic esbuild trick: use an impossible to guess key for string magic */
46const uniqueKey = crypto.randomUUID().replaceAll("-", "");
47
48const renderers = {
49 A: artifactRef,
50 Q: questionRef,
51 R: labelledRedaction,
52} as const;
53
54export function renderMarkdown(source: string) {
55 const tagNames: markodown.CloverQuestionExtensions = {
56 question: "q-",
57 artifactRef: uniqueKey + "_A",
58 questionRef: uniqueKey + "_Q",
59 labelledRedaction: uniqueKey + "_R",
60 };
61 const result = markodown.transform({
62 source,
63 markdownOnly: true,
64 cloverExtensions: tagNames,
65 });
66 if (!result.success) {
67 throw new Error(JSON.stringify(result.errors));
68 }
69 return result.text
70 .replace(
71 new RegExp(`<${uniqueKey}_([AQR])>([^<]*)<\\/${uniqueKey}_[AQR]>`, "g"),
72 (_, type: "A" | "Q" | "R", content: string) => {
73 return render.sync(renderers[type](content)).text;
74 },
75 );
76}
77
78import * as markodown from "@clo/markodown";
79import * as render from "@clo/lib/render";
80import { artifactMap } from "./artifacts.ts";
81import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts";
src/q+a/scripts/editor.client.ts created+95
......@@ -0,0 +1,95 @@
1import type { ScriptPayload } from "#src/q+a/views/editor.marko";
2import QuestionRender from "#src/q+a/tags/question.marko";
3
4declare const payload: ScriptPayload;
5const date = new Date(payload.date);
6
7const main = document.getElementById("edit-grid")! as HTMLDivElement;
8const preview = document.getElementById("preview")! as HTMLDivElement;
9const editor = document.getElementById("editor") as HTMLTextAreaElement;
10
11async function updatePreview(text: string) {
12 preview.innerHTML = await QuestionRender.render({
13 question: {
14 id: payload.id,
15 qmid: payload.qmid,
16 text: text,
17 type: payload.type,
18 date,
19 },
20 editor: true,
21 });
22}
23updatePreview(payload.text);
24
25editor.value = payload.text;
26editor.addEventListener("input", () => {
27 updatePreview(editor.value);
28});
29editor.focus();
30
31(globalThis as any).onCommitQuestion = wrapAction(async () => {
32 const text = editor.value;
33 const res = await fetch(`/admin/q+a/${payload.id}`, {
34 method: "PATCH",
35 headers: {
36 Accept: "application/json",
37 "Content-Type": "application/json",
38 },
39 body: JSON.stringify({
40 text,
41 type: payload.type,
42 }),
43 });
44 if (!res.ok) {
45 throw new Error("Failed to update question, status: " + res.status);
46 }
47 if (location.search.includes("return=inbox")) {
48 location.href = "/admin/q+a/inbox";
49 } else {
50 location.href = "/q+a#q" + payload.id;
51 }
52});
53(globalThis as any).onDelete = wrapAction(async () => {
54 if (confirm("Are you sure you want to delete this question?")) {
55 const res = await fetch(`/admin/q+a/${payload.id}`, {
56 method: "DELETE",
57 headers: {
58 Accept: "application/json",
59 },
60 });
61 if (!res.ok) {
62 throw new Error("Failed to delete question, status: " + res.status);
63 }
64 location.href = document.referrer || "/admin/q+a";
65 }
66});
67(globalThis as any).onTypeChange = () => {
68 payload.type = parseInt(
69 (document.getElementById("type") as HTMLSelectElement).value,
70 );
71 updatePreview(view.state.doc.toString());
72};
73
74function wrapAction(cb: () => Promise<void>) {
75 return async () => {
76 main.style.opacity = "0.5";
77 main.style.pointerEvents = "none";
78 const inputs = main.querySelectorAll(
79 "button,select,input",
80 ) as NodeListOf<HTMLButtonElement>;
81 inputs.forEach((b) => {
82 b.disabled = true;
83 });
84 try {
85 await cb();
86 } catch (e: any) {
87 main.style.opacity = "1";
88 main.style.pointerEvents = "auto";
89 inputs.forEach((b) => {
90 b.disabled = false;
91 });
92 alert(e.message);
93 }
94 };
95}
src/q+a/scripts/editor.client.tsx deleted-95
......@@ -1,95 +0,0 @@
1import type { ScriptPayload } from "#src/q+a/views/editor.marko";
2import QuestionRender from "#src/q+a/tags/question.marko";
3
4declare const payload: ScriptPayload;
5const date = new Date(payload.date);
6
7const main = document.getElementById("edit-grid")! as HTMLDivElement;
8const preview = document.getElementById("preview")! as HTMLDivElement;
9const editor = document.getElementById("editor") as HTMLTextAreaElement;
10
11async function updatePreview(text: string) {
12 preview.innerHTML = await QuestionRender.render({
13 question: {
14 id: payload.id,
15 qmid: payload.qmid,
16 text: text,
17 type: payload.type,
18 date,
19 },
20 editor: true,
21 });
22}
23updatePreview(payload.text);
24
25editor.value = payload.text;
26editor.addEventListener("input", () => {
27 updatePreview(editor.value);
28});
29editor.focus();
30
31(globalThis as any).onCommitQuestion = wrapAction(async () => {
32 const text = editor.value;
33 const res = await fetch(`/admin/q+a/${payload.id}`, {
34 method: "PATCH",
35 headers: {
36 Accept: "application/json",
37 "Content-Type": "application/json",
38 },
39 body: JSON.stringify({
40 text,
41 type: payload.type,
42 }),
43 });
44 if (!res.ok) {
45 throw new Error("Failed to update question, status: " + res.status);
46 }
47 if (location.search.includes("return=inbox")) {
48 location.href = "/admin/q+a/inbox";
49 } else {
50 location.href = "/q+a#q" + payload.id;
51 }
52});
53(globalThis as any).onDelete = wrapAction(async () => {
54 if (confirm("Are you sure you want to delete this question?")) {
55 const res = await fetch(`/admin/q+a/${payload.id}`, {
56 method: "DELETE",
57 headers: {
58 Accept: "application/json",
59 },
60 });
61 if (!res.ok) {
62 throw new Error("Failed to delete question, status: " + res.status);
63 }
64 location.href = document.referrer || "/admin/q+a";
65 }
66});
67(globalThis as any).onTypeChange = () => {
68 payload.type = parseInt(
69 (document.getElementById("type") as HTMLSelectElement).value,
70 );
71 updatePreview(view.state.doc.toString());
72};
73
74function wrapAction(cb: () => Promise<void>) {
75 return async () => {
76 main.style.opacity = "0.5";
77 main.style.pointerEvents = "none";
78 const inputs = main.querySelectorAll(
79 "button,select,input",
80 ) as NodeListOf<HTMLButtonElement>;
81 inputs.forEach((b) => {
82 b.disabled = true;
83 });
84 try {
85 await cb();
86 } catch (e: any) {
87 main.style.opacity = "1";
88 main.style.pointerEvents = "auto";
89 inputs.forEach((b) => {
90 b.disabled = false;
91 });
92 alert(e.message);
93 }
94 };
95}
src/q+a/tags/question.marko+1-1
......@@ -39,5 +39,5 @@ export const transitionDate = 1735639200000;
3939
4040import type { Question } from "#src/q+a/models/Question.ts";
4141import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "#src/q+a/format.ts";
42import * as markdown from "#src/q+a/markdown.tsx";
42import * as markdown from "#src/q+a/markdown.ts";
4343import { addScript as Script } from "#sitegen"
src/q+a/views/editor.marko+1-2
......@@ -59,9 +59,8 @@ export interface ScriptPayload {
5959 <question editor ...{ question } />
6060 </main>
6161</div>
62<html-script>self.payload=${devalue.uneval(payload)}</html-script>
62<html-script>self.payload=${JSON.stringify(payload).replaceAll('</script>', '<\\/script>')}</html-script>
6363<html-script src="/js/q+a/scripts/editor.js" type="module" />
6464
65import * as devalue from 'devalue';
6665import { type PendingQuestion } from "#src/q+a/models/PendingQuestion.ts";
6766import { Question, QuestionType } from "#src/q+a/models/Question.ts";