throw in the file viewer

This commit is contained in:
chloe caruso 2025-06-15 23:42:10 -07:00
parent 7f5011bace
commit c7dfbe1090
82 changed files with 58218 additions and 5 deletions

BIN
meow.txt Normal file

Binary file not shown.

514
package-lock.json generated
View file

@ -8,6 +8,7 @@
"@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"blurhash": "^2.0.5",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5",
@ -15,6 +16,7 @@
"hono": "^4.7.11",
"marko": "^6.0.20",
"puppeteer": "^24.10.1",
"sharp": "^0.34.2",
"unique-names-generator": "^4.7.1"
},
"devDependencies": {
@ -487,6 +489,16 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -899,6 +911,402 @@
"hono": "^4"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
"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.1.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
"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.1.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
"cpu": [
"ppc64"
],
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
"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.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
"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.1.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
"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.1.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
"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.1.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
"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.1.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
"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.1.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
"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.1.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.3"
},
"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.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
"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.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
"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.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
"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/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -1385,6 +1793,12 @@
"node": ">=10.0.0"
}
},
"node_modules/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
@ -1596,6 +2010,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1614,6 +2041,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -1739,6 +2176,15 @@
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/devalue": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
@ -3723,6 +4169,74 @@
"semver": "bin/semver.js"
}
},
"node_modules/sharp": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.2",
"@img/sharp-darwin-x64": "0.34.2",
"@img/sharp-libvips-darwin-arm64": "1.1.0",
"@img/sharp-libvips-darwin-x64": "1.1.0",
"@img/sharp-libvips-linux-arm": "1.1.0",
"@img/sharp-libvips-linux-arm64": "1.1.0",
"@img/sharp-libvips-linux-ppc64": "1.1.0",
"@img/sharp-libvips-linux-s390x": "1.1.0",
"@img/sharp-libvips-linux-x64": "1.1.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
"@img/sharp-linux-arm": "0.34.2",
"@img/sharp-linux-arm64": "0.34.2",
"@img/sharp-linux-s390x": "0.34.2",
"@img/sharp-linux-x64": "0.34.2",
"@img/sharp-linuxmusl-arm64": "0.34.2",
"@img/sharp-linuxmusl-x64": "0.34.2",
"@img/sharp-wasm32": "0.34.2",
"@img/sharp-win32-arm64": "0.34.2",
"@img/sharp-win32-ia32": "0.34.2",
"@img/sharp-win32-x64": "0.34.2"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",

View file

@ -4,6 +4,7 @@
"@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"blurhash": "^2.0.5",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5",
@ -11,6 +12,7 @@
"hono": "^4.7.11",
"marko": "^6.0.20",
"puppeteer": "^24.10.1",
"sharp": "^0.34.2",
"unique-names-generator": "^4.7.1"
},
"devDependencies": {

View file

@ -17,7 +17,7 @@ JavaScript into HTML. Attributes are JavaScript expressions.
```marko
<div>
// `input` is like props, but in global scope
// `input` is like props, but given in the top-level scope
<time datetime=input.date.toISOString()>
// Interpolation with JS template string syntax
${formatTimeNicely(input.date)}
@ -28,6 +28,12 @@ JavaScript into HTML. Attributes are JavaScript expressions.
// Capital letter variables for imported components
<MarkdownContent message=input.message />
// Components also can be auto-imported by lowercase.
// This will look upwards for a `tags/` folder containing
// "custom-footer.marko", similar to how Node.js finds
// package names in all upwards `node_modules` folders.
<custom-footer />
</div>
// ESM `import` / `export` just work as expected.

425
src/file-viewer/backend.tsx Normal file
View file

@ -0,0 +1,425 @@
import { type Context, Hono } from "hono";
import * as path from "node:path";
import { etagMatches, serveAsset } from "../assets.ts";
import { FilePermissions, MediaFile } from "../db.ts";
import { renderDynamicPage } from "../framework/dynamic-pages.ts";
import { renderToStringSync } from "../framework/render-to-string.ts";
import { MediaPanel } from "../pages-dynamic/file_viewer.tsx";
import mimeTypeDb from "./mime.json" with { type: "json" };
import { Speedbump } from "./cotyledon.tsx";
import { hasAsset } from "../assets.ts";
import { CompressionFormat, fetchFile, prefetchFile } from "./cache.ts";
import { requireFriendAuth } from "../journal/backend.ts";
const app = new Hono();
interface APIDirectoryList {
path: string;
readme: string | null;
files: APIFile[];
}
interface APIFile {
basename: string;
dir: boolean;
time: number;
size: number;
duration: number | null;
}
function checkCotyledonCookie(c: Context) {
const cookie = c.req.header("Cookie");
if (!cookie) return false;
const cookies = cookie.split("; ").map((x) => x.split("="));
return cookies.some(
(kv) => kv[0].trim() === "cotyledon" && kv[1].trim() === "agree",
);
}
function isCotyledonPath(path: string) {
if (path === "/cotyledon") return true;
const year = path.match(/^\/(\d{4})($|\/)/);
if (!year) return false;
const yearInt = parseInt(year[1]);
if (yearInt < 2025 && yearInt >= 2017) return true;
return false;
}
app.post("/file/cotyledon", async (c) => {
c.res = new Response(null, {
status: 200,
headers: {
"Set-Cookie": "cotyledon=agree; Path=/",
},
});
});
app.get("/file/*", async (c, next) => {
if (c.req.header("User-Agent")?.toLowerCase()?.includes("discordbot")) {
return next();
}
let rawFilePath = c.req.path.slice(5) || "/";
if (rawFilePath.endsWith("$partial")) {
return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
}
let hasCotyledonCookie = checkCotyledonCookie(c);
if (isCotyledonPath(rawFilePath)) {
if (!hasCotyledonCookie) {
return serveAsset(c, "/file/cotyledon_speedbump", 403);
} else if (rawFilePath === "/cotyledon") {
return serveAsset(c, "/file/cotyledon_enterance", 200);
}
}
while (rawFilePath.length > 1 && rawFilePath.endsWith("/")) {
rawFilePath = rawFilePath.slice(0, -1);
}
const file = MediaFile.getByPath(rawFilePath);
if (!file) {
// perhaps a specific 404 page for media files?
return next();
}
const permissions = FilePermissions.getByPrefix(rawFilePath);
if (permissions !== 0) {
const friendAuthChallenge = requireFriendAuth(c);
if (friendAuthChallenge) return friendAuthChallenge;
}
// File listings
if (file.kind === MediaFile.Kind.directory) {
if (c.req.header("Accept")?.includes("application/json")) {
const json = {
path: file.path,
files: file.getPublicChildren().map((f) => ({
basename: f.basename,
dir: f.kind === MediaFile.Kind.directory,
time: f.date.getTime(),
size: f.size,
duration: f.duration ? f.duration : null,
})),
readme: file.contents ? file.contents : null,
} satisfies APIDirectoryList;
return c.json(json);
}
c.res = await renderDynamicPage(c.req.raw, "file_viewer", {
file,
hasCotyledonCookie,
});
return;
}
// Redirect to directory list for regular files if client accepts HTML
let viewMode = c.req.query("view");
if (c.req.query("dl") !== undefined) {
viewMode = "download";
}
if (viewMode == undefined && c.req.header("Accept")?.includes("text/html")) {
prefetchFile(file.path);
c.res = await renderDynamicPage(c.req.raw, "file_viewer", {
file,
hasCotyledonCookie,
});
return;
}
const download = viewMode === "download";
const etag = file.hash;
const filePath = file.path;
const expectedSize = file.size;
let encoding = decideEncoding(c.req.header("Accept-Encoding"));
let sizeHeader = encoding === "raw"
? expectedSize
// Size cannot be known because of compression modes
: undefined;
// Etag
{
const ifNoneMatch = c.req.header("If-None-Match");
if (ifNoneMatch && etagMatches(etag, ifNoneMatch)) {
c.res = new Response(null, {
status: 304,
statusText: "Not Modified",
headers: fileHeaders(file, download, sizeHeader),
});
return;
}
}
// Head
if (c.req.method === "HEAD") {
c.res = new Response(null, {
headers: fileHeaders(file, download, sizeHeader),
});
return;
}
// Prevalidate range requests
let rangeHeader = c.req.header("Range") ?? null;
if (rangeHeader) encoding = "raw";
const ifRangeHeader = c.req.header("If-Range");
if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) {
// > If the condition is not fulfilled, the full resource is
// > sent back with a 200 OK status.
rangeHeader = null;
}
let foundFile;
while (true) {
let second = false;
try {
foundFile = await fetchFile(filePath, encoding);
if (second) {
console.warn(`File ${filePath} has missing compression: ${encoding}`);
}
break;
} catch (error) {
if (encoding !== "raw") {
encoding = "raw";
sizeHeader = file.size;
second = true;
continue;
}
return c.text(
"internal server error: this file is present in the database but could not be fetched",
);
}
}
const [streamOrBuffer, actualEncoding, src] = foundFile;
encoding = actualEncoding;
// Range requests
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
// Compression is skipped because it's a confusing, but solvable problem.
// See https://stackoverflow.com/questions/33947562/is-it-possible-to-send-http-response-using-gzip-and-byte-ranges-at-the-same-time
if (rangeHeader) {
const ranges = parseRange(rangeHeader, file.size);
// TODO: multiple ranges
if (ranges && ranges.length === 1) {
return (c.res = handleRanges(ranges, file, streamOrBuffer, download));
}
}
// Respond in a streaming fashion
c.res = new Response(streamOrBuffer, {
headers: {
...fileHeaders(file, download, sizeHeader),
...(encoding !== "raw" && {
"Content-Encoding": encoding,
}),
"X-Cache": src,
},
});
});
app.get("/canvas/:script", async (c, next) => {
const script = c.req.param("script");
if (!hasAsset(`/js/canvas/${script}.js`)) {
return next();
}
return renderDynamicPage(c.req.raw, "canvas", {
script,
});
});
function decideEncoding(encodings: string | undefined): CompressionFormat {
if (encodings?.includes("zstd")) return "zstd";
if (encodings?.includes("gzip")) return "gzip";
return "raw";
}
function fileHeaders(
file: MediaFile,
download: boolean,
size: number | undefined = file.size,
) {
return {
Vary: "Accept-Encoding, Accept",
"Content-Type": mimeType(file.path),
"Content-Length": size.toString(),
ETag: file.hash,
"Last-Modified": file.date.toUTCString(),
...(download && {
"Content-Disposition": `attachment; filename="${file.basename}"`,
}),
};
}
function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) {
// etag
if (ifRangeHeader[0] === '"') {
return ifRangeHeader.slice(1, -1) !== file.hash;
}
// date
return new Date(ifRangeHeader) < file.date;
}
/** The end is inclusive */
type Ranges = Array<[start: number, end: number]>;
function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
const [unit, ranges] = rangeHeader.split("=");
if (unit !== "bytes") return null;
const result: Array<[start: number, end: number]> = [];
const rangeParts = ranges.split(",");
for (const range of rangeParts) {
const split = range.split("-");
if (split.length !== 2) return null;
const [start, end] = split;
if (start === "" && end === "") return null;
const parsedRange: [number, number] = [
start === "" ? fileSize - +end : +start,
end === "" ? fileSize - 1 : +end,
];
result.push(parsedRange);
}
// Validate that ranges do not intersect
result.sort((a, b) => a[0] - b[0]);
for (let i = 1; i < result.length; i++) {
if (result[i][0] <= result[i - 1][1]) {
return null;
}
}
return result;
}
function handleRanges(
ranges: Ranges,
file: MediaFile,
streamOrBuffer: ReadableStream | Buffer,
download: boolean,
): Response {
// TODO: multiple ranges
const rangeSize = ranges.reduce((a, b) => a + (b[1] - b[0] + 1), 0);
const rangeBody = streamOrBuffer instanceof ReadableStream
? applySingleRangeToStream(streamOrBuffer, ranges)
: applyRangesToBuffer(streamOrBuffer, ranges, rangeSize);
return new Response(rangeBody, {
status: 206,
headers: {
...fileHeaders(file, download, rangeSize),
"Content-Range": `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
},
});
}
function applyRangesToBuffer(
buffer: Buffer,
ranges: Ranges,
rangeSize: number,
): Uint8Array {
const result = new Uint8Array(rangeSize);
let offset = 0;
for (const [start, end] of ranges) {
result.set(buffer.slice(start, end + 1), offset);
offset += end - start + 1;
}
return result;
}
function applySingleRangeToStream(
stream: ReadableStream,
ranges: Ranges,
): ReadableStream {
let reader: ReadableStreamDefaultReader<Uint8Array>;
let position = 0;
const [start, end] = ranges[0];
return new ReadableStream({
async start(controller) {
reader = stream.getReader();
try {
while (position <= end) {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
const buffer = new Uint8Array(value);
const bufferStart = position;
const bufferEnd = position + buffer.length - 1;
position += buffer.length;
if (bufferEnd < start) {
continue;
}
if (bufferStart > end) {
break;
}
const sendStart = Math.max(0, start - bufferStart);
const sendEnd = Math.min(buffer.length - 1, end - bufferStart);
if (sendStart <= sendEnd) {
controller.enqueue(buffer.slice(sendStart, sendEnd + 1));
}
}
controller.close();
} catch (error) {
controller.error(error);
} finally {
reader.releaseLock();
}
},
cancel() {
reader?.releaseLock();
},
});
}
function mimeType(file: string) {
return (mimeTypeDb as any)[path.extname(file)] ?? "application/octet-stream";
}
function getPartialPage(c: Context, rawFilePath: string) {
if (isCotyledonPath(rawFilePath)) {
if (!checkCotyledonCookie(c)) {
let root = Speedbump();
// Remove the root element, it's created client side!
root = root.props.children;
const html = renderToStringSync(root);
c.header("X-Cotyledon", "true");
return c.html(html);
}
}
const file = MediaFile.getByPath(rawFilePath);
const permissions = FilePermissions.getByPrefix(rawFilePath);
if (permissions !== 0) {
const friendAuthChallenge = requireFriendAuth(c);
if (friendAuthChallenge) return friendAuthChallenge;
}
if (rawFilePath.endsWith("/")) {
rawFilePath = rawFilePath.slice(0, -1);
}
if (!file) {
return c.json({ error: "File not found" }, 404);
}
let root = MediaPanel({
file,
isLast: true,
activeFilename: null,
hasCotyledonCookie: rawFilePath === "" && checkCotyledonCookie(c),
});
// Remove the root element, it's created client side!
root = root.props.children;
const html = renderToStringSync(root);
return c.html(html);
}
export { app as mediaApp };

View file

@ -0,0 +1,83 @@
import * as path from "node:path";
import { cache, MediaFile } from "../db";
// Function to get file extension statistics
function getExtensionStats() {
// Get all files (not directories) from the database
const query = `
SELECT path FROM media_files
WHERE kind = ${MediaFile.Kind.file}
`;
// Use raw query to get all file paths
const rows = cache.query(query).all() as { path: string }[];
// Count extensions
const extensionCounts: Record<string, number> = {};
for (const row of rows) {
const extension = path.extname(row.path).toLowerCase();
extensionCounts[extension] = (extensionCounts[extension] || 0) + 1;
}
// Sort extensions by count (descending)
const sortedExtensions = Object.entries(extensionCounts)
.sort((a, b) => b[1] - a[1]);
return {
totalFiles: rows.length,
extensions: sortedExtensions,
};
}
// Function to print a visual table
function printExtensionTable() {
const stats = getExtensionStats();
// Calculate column widths
const extensionColWidth = Math.max(
...stats.extensions.map(([ext]) => ext.length),
"Extension".length,
) + 2;
const countColWidth = Math.max(
...stats.extensions.map(([_, count]) => count.toString().length),
"Count".length,
) + 2;
const percentColWidth = "Percentage".length + 2;
// Print header
console.log("MediaFile Extension Statistics");
console.log(`Total files: ${stats.totalFiles}`);
console.log();
// Print table header
console.log(
"Extension".padEnd(extensionColWidth) +
"Count".padEnd(countColWidth) +
"Percentage".padEnd(percentColWidth),
);
// Print separator
console.log(
"-".repeat(extensionColWidth) +
"-".repeat(countColWidth) +
"-".repeat(percentColWidth),
);
// Print rows
for (const [extension, count] of stats.extensions) {
const percentage = ((count / stats.totalFiles) * 100).toFixed(2);
const ext = extension || "(no extension)";
console.log(
ext.padEnd(extensionColWidth) +
count.toString().padEnd(countColWidth) +
`${percentage}%`.padEnd(percentColWidth),
);
}
}
// Run the program
printExtensionTable();

1015
src/file-viewer/bin/scan.ts Normal file

File diff suppressed because it is too large Load diff

418
src/file-viewer/cache.ts Normal file
View file

@ -0,0 +1,418 @@
import { Agent, get } from "node:https";
import * as fs from "node:fs";
import * as path from "node:path";
import { Buffer } from "node:buffer";
import type { ClientRequest } from "node:http";
import { LRUCache } from "lru-cache";
import { open } from "node:fs/promises";
import { createHash } from "node:crypto";
import { scoped } from "@paperclover/console";
import { escapeUri } from "./share.ts";
declare const Deno: any;
const sourceOfTruth = "https://nas.paperclover.net:43250";
const caCert = fs.readFileSync(path.join(import.meta.dirname, "cert.pem"));
const diskCacheRoot = path.join(import.meta.dirname, "../.clover/filecache/");
const diskCacheMaxSize = 14 * 1024 * 1024 * 1024; // 14GB
const ramCacheMaxSize = 1 * 1024 * 1024 * 1024; // 1.5GB
const loadInProgress = new Map<
string,
Promise<{ stream: ReadableStream }> | { stream: ReadableStream }
>();
// Disk cache serializes the access times
const diskCacheState: Record<string, [size: number, lastAccess: number]> =
loadDiskCacheState();
const diskCache = new LRUCache<string, number>({
maxSize: diskCacheMaxSize,
ttl: 0,
sizeCalculation: (value) => value,
dispose: (_, key) => {
delete diskCacheState[key];
},
onInsert: (size, key) => {
diskCacheState[key] = [size, Date.now()];
},
});
const ramCache = new LRUCache<string, Buffer>({
maxSize: ramCacheMaxSize,
ttl: 0,
sizeCalculation: (value) => value.byteLength,
});
let diskCacheFlush: NodeJS.Timeout | undefined;
{
// Initialize the disk cache by validating all files exist, and then
// inserting them in last to start order. State is repaired pessimistically.
const toDelete = new Set(Object.keys(diskCacheState));
fs.mkdirSync(diskCacheRoot, { recursive: true });
for (
const file of fs.readdirSync(diskCacheRoot, {
recursive: true,
encoding: "utf-8",
})
) {
const key = file.split("/").pop()!;
if (key.length !== 40) continue;
const entry = diskCacheState[key];
if (!entry) {
fs.rmSync(path.join(diskCacheRoot, file), {
recursive: true,
force: true,
});
delete diskCacheState[key];
continue;
}
toDelete.delete(key);
}
for (const key of toDelete) {
delete diskCacheState[key];
}
saveDiskCacheState();
const sorted = Object.keys(diskCacheState).sort((a, b) =>
diskCacheState[b][1] - diskCacheState[a][1]
);
for (const key of sorted) {
diskCache.set(key, diskCacheState[key][0]);
}
}
export type CacheSource = "ram" | "disk" | "miss" | "lan" | "flight";
export type CompressionFormat = "gzip" | "zstd" | "raw";
const compressionFormatMap = {
gzip: "gz",
zstd: "zstd",
raw: "file",
} as const;
const log = scoped("file_cache");
const lanMount = "/Volumes/clover/Published";
const hasLanMount = fs.existsSync(lanMount);
/**
* Fetches a file with the given compression format.
* Uncompressed files are never persisted to disk.
*
* Returns a promise to either:
* - Buffer: the data is from RAM cache
* - ReadableStream: the data is being streamed in from disk/server
*
* Additionally, returns a string indicating the source of the data, for debugging.
*
* Callers must be able to consume both output types.
*/
export async function fetchFile(
pathname: string,
format: CompressionFormat = "raw",
): Promise<
[Buffer | ReadableStream, encoding: CompressionFormat, src: CacheSource]
> {
// 1. Ram cache
const cacheKey = hashKey(`${pathname}:${format}`);
const ramCacheHit = ramCache.get(cacheKey);
if (ramCacheHit) {
log(`ram hit: ${format}${pathname}`);
return [ramCacheHit, format, "ram"];
}
// 2. Tee an existing loading stream.
const inProgress = loadInProgress.get(cacheKey);
if (inProgress) {
const stream = await inProgress;
const [stream1, stream2] = stream.stream.tee();
loadInProgress.set(cacheKey, { stream: stream2 });
log(`in-flight copy: ${format}${pathname}`);
return [stream1, format, "flight"];
}
// 3. Disk cache + Load into ram cache.
if (format !== "raw") {
const diskCacheHit = diskCache.get(cacheKey);
if (diskCacheHit) {
diskCacheState[cacheKey] = [diskCacheHit, Date.now()];
saveDiskCacheStateLater();
log(`disk hit: ${format}/${pathname}`);
return [
startInProgress(
cacheKey,
new ReadableStream({
start: async (controller) => {
const stream = fs.createReadStream(
path.join(diskCacheRoot, cacheKey),
);
const chunks: Buffer[] = [];
stream.on("data", (chunk) => {
controller.enqueue(chunk);
chunks.push(chunk as Buffer);
});
stream.on("end", () => {
controller.close();
ramCache.set(cacheKey, Buffer.concat(chunks));
finishInProgress(cacheKey);
});
stream.on("error", (error) => {
controller.error(error);
});
},
}),
),
format,
"disk",
];
}
}
// 4. Lan Mount (access files that prod may not have)
if (hasLanMount) {
log(`lan hit: ${format}/${pathname}`);
return [
startInProgress(
cacheKey,
new ReadableStream({
start: async (controller) => {
const stream = fs.createReadStream(
path.join(lanMount, pathname),
);
const chunks: Buffer[] = [];
stream.on("data", (chunk) => {
controller.enqueue(chunk);
chunks.push(chunk as Buffer);
});
stream.on("end", () => {
controller.close();
ramCache.set(cacheKey, Buffer.concat(chunks));
finishInProgress(cacheKey);
});
stream.on("error", (error) => {
controller.error(error);
});
},
}),
),
"raw",
"lan",
];
}
// 4. Fetch from server
const url = `${compressionFormatMap[format]}${escapeUri(pathname)}`;
log(`miss: ${format}${pathname}`);
const response = await startInProgress(cacheKey, fetchFileUncached(url));
const [stream1, stream2] = response.tee();
handleDownload(cacheKey, format, stream2);
return [stream1, format, "miss"];
}
export async function prefetchFile(
pathname: string,
format: CompressionFormat = "zstd",
) {
const cacheKey = hashKey(`${pathname}:${format}`);
const ramCacheHit = ramCache.get(cacheKey);
if (ramCacheHit) {
return;
}
if (hasLanMount) return;
const url = `${compressionFormatMap[format]}${pathname}`;
log(`prefetch: ${format}${pathname}`);
const stream2 = await startInProgress(cacheKey, fetchFileUncached(url));
handleDownload(cacheKey, format, stream2);
}
async function handleDownload(
cacheKey: string,
format: CompressionFormat,
stream2: ReadableStream,
) {
let chunks: Buffer[] = [];
if (format !== "raw") {
const file = await open(path.join(diskCacheRoot, cacheKey), "w");
try {
for await (const chunk of stream2) {
await file.write(chunk);
chunks.push(chunk);
}
} finally {
file.close();
}
} else {
for await (const chunk of stream2) {
chunks.push(chunk);
}
}
const final = Buffer.concat(chunks);
chunks.length = 0;
ramCache.set(cacheKey, final);
if (format !== "raw") {
diskCache.set(cacheKey, final.byteLength);
}
finishInProgress(cacheKey);
}
function hashKey(key: string): string {
return createHash("sha1").update(key).digest("hex");
}
function startInProgress<T extends Promise<ReadableStream> | ReadableStream>(
cacheKey: string,
promise: T,
): T {
if (promise instanceof Promise) {
let resolve2: (stream: { stream: ReadableStream }) => void;
let reject2: (error: Error) => void;
const stream2Promise = new Promise<{ stream: ReadableStream }>(
(resolve, reject) => {
resolve2 = resolve;
reject2 = reject;
},
);
const stream1Promise = new Promise<ReadableStream>((resolve, reject) => {
promise.then((stream) => {
const [stream1, stream2] = stream.tee();
const stream2Obj = { stream: stream2 };
resolve2(stream2Obj);
loadInProgress.set(cacheKey, stream2Obj);
resolve(stream1);
}, reject);
});
loadInProgress.set(cacheKey, stream2Promise);
return stream1Promise as T;
} else {
const [stream1, stream2] = promise.tee();
loadInProgress.set(cacheKey, { stream: stream2 });
return stream1 as T;
}
}
function loadDiskCacheState(): Record<
string,
[size: number, lastAccess: number]
> {
try {
const state = JSON.parse(
fs.readFileSync(path.join(diskCacheRoot, "state.json"), "utf-8"),
);
return state;
} catch (error) {
return {};
}
}
function saveDiskCacheStateLater() {
if (diskCacheFlush) {
return;
}
diskCacheFlush = setTimeout(() => {
saveDiskCacheState();
}, 60_000) as NodeJS.Timeout;
if (diskCacheFlush.unref) {
diskCacheFlush.unref();
}
}
process.on("exit", () => {
saveDiskCacheState();
});
function saveDiskCacheState() {
fs.writeFileSync(
path.join(diskCacheRoot, "state.json"),
JSON.stringify(diskCacheState),
);
}
function finishInProgress(cacheKey: string) {
loadInProgress.delete(cacheKey);
}
// Self signed certificate must be trusted to be able to request the above URL.
//
// Unfortunately, Bun and Deno are both not node.js compatible, so those two
// runtimes need fallback implementations. The fallback implementations calls
// fetch with the `agent` value as the RequestInit. Since `fetch` decompresses
// the body for you, it must be disabled.
const agent: any = typeof Bun !== "undefined"
? {
// Bun has two non-standard fetch extensions
decompress: false,
tls: {
ca: caCert,
},
}
// TODO: https://github.com/denoland/deno/issues/12291
// : typeof Deno !== "undefined"
// ? {
// // Deno configures through the non-standard `client` extension
// client: Deno.createHttpClient({
// caCerts: [caCert.toString()],
// }),
// }
// Node.js supports node:http
: new Agent({
ca: caCert,
});
function fetchFileNode(pathname: string): Promise<ReadableStream> {
return new Promise((resolve, reject) => {
const request: ClientRequest = get(`${sourceOfTruth}/${pathname}`, {
agent,
});
request.on("response", (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to fetch ${pathname}`));
return;
}
const stream = new ReadableStream({
start(controller) {
response.on("data", (chunk) => {
controller.enqueue(chunk);
});
response.on("end", () => {
controller.close();
});
response.on("error", (error) => {
controller.error(error);
reject(error);
});
},
});
resolve(stream);
});
request.on("error", (error) => {
reject(error);
});
});
}
async function fetchFileDenoBun(pathname: string): Promise<ReadableStream> {
const req = await fetch(`${sourceOfTruth}/${pathname}`, agent);
if (!req.ok) {
throw new Error(`Failed to fetch ${pathname}`);
}
return req.body!;
}
const fetchFileUncached =
typeof Bun !== "undefined" || typeof Deno !== "undefined"
? fetchFileDenoBun
: fetchFileNode;
export async function toBuffer(
stream: ReadableStream | Buffer,
): Promise<Buffer> {
if (!(stream instanceof ReadableStream)) {
return stream;
}
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}

23
src/file-viewer/cert.pem Normal file
View file

@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIUBaaOXVkkE+6yarNyvzofETb+WLEwDQYJKoZIhvcNAQEL
BQAwdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0aDEhMB8GCSqG
SIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MB4XDTI1MDQyNzIxNTU0MFoXDTM1
MDQyNTIxNTU0MFowdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYD
VQQHDARDaXR5MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0
aDEhMB8GCSqGSIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7lLwx8XwsuTeaIxTsHDL+Lx7eblsJ0XylVm
0/iIJS1Mrq6Be9St6vDWK/BWqqAn+MdqzSfLMy8EKazuHKtbTm2vlUIkjw28SoWP
6cRSCLx4hFGbF4tmRO+Bo+/4PpHPnheeolkjJ+CLO87tZ752D9JzjVND+WIj1QO+
bm+JBIi1TFREPh22/fSZBRpaRgqHcUEhICaiXaufvxQ6eihQfGSe00I7zRzGgnMl
51xjzkKkXd+r/FwTykd8ScJN25FMVDLsfJR59//geAZXYS25gQ4YL6R8u7ijidlS
IoDG8N+Fzw7W4yI+y8fIN4W1x/HsjiQ665CuWY3TMYo98OaGwwIDAQABo0kwRzAm
BgNVHREEHzAdggZ6ZW5pdGiCE25hcy5wYXBlcmNsb3Zlci5uZXQwHQYDVR0OBBYE
FDXkgNsMYZv1Pr+95RCCk7eHACGOMA0GCSqGSIb3DQEBCwUAA4IBAQB6942odKyD
TudifxRXbvcVe9LxSd7NimxRZzM5wTgA5KkxQT4CBM2wEPH/7e7Q/8scB9HbH2uP
f2vixoCM+Z3BWiYHFFk+1pf2myUdiFV2BC9g80txEerRkGLc18V6CdYNJ9wNPkiO
LW/RzXfEv+sqhaXh8dA46Ruz6SAbmscTMMYW4e9VYR+1p4Sm5UpTxrHzeg21YJKn
ud8kO1r7RhVgUGzkAzNaIMiBuJqGGdD5yV7Ng5C/DlJ9AAeYu1diM5LkIKjf+/8M
t/3l4eXS3Lda6+21rDvmfoK4Za6CAhcwgXIpqiRixE2MQNsxZ2XiJBVQHPrh8xYk
L5fq8KTGFwtd
-----END CERTIFICATE-----

View file

@ -0,0 +1,291 @@
export function Speedbump() {
return (
<div class="panel last">
<div className="header">
an interlude
</div>
<div className="content file-view file-view-text speedbump">
<canvas
style="linear-gradient(45deg, #111318, #181f20)"
data-canvas="cotyledon"
>
</canvas>
<header>
<h1>cotyledon</h1>
</header>
<div id="captcha" style="display: none;">
<p style="max-width:480px">
please prove you're not a robot by selecting all of the images with
four-leaf clovers, until there are only regular clovers.
<noscript>
this will require javascript enabled on your computer to verify
the mouse clicks.
</noscript>
</p>
<div className="enter-container">
<div className="image-grid">
<button>
<img src="/captcha/image/1.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/2.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/3.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/4.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/5.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/6.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/7.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/8.jpeg" alt="a four-leaf clover" />
</button>
<button>
<img src="/captcha/image/9.jpeg" alt="a four-leaf clover" />
</button>
</div>
</div>
<div class="enter-container">
<button id="enter2">all done</button>
</div>
</div>
<div id="first">
<p>
this place is sacred, but dangerous. i have to keep visitors to an
absolute minimum; you'll get dust on all the artifacts.
</p>
<p>
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
</p>
<p style="font-size:0.9rem;">
(in english: please do not store downloads after you're done viewing
them)
</p>
<div class="enter-container">
<button id="enter">break my boundaries</button>
</div>
</div>
</div>
</div>
);
}
export function Readme() {
return (
<div class="panel last">
<div className="header">
cotyledon
</div>
<div className="content file-view file-view-text">
<div style="max-width: 71ch;padding:3rem;font-family:rmo,monospace">
<p style="margin-top:0">
welcome to the archive. if this is your first time here, i recommend
starting in '<a href="/file/2017">2017</a>' and going
chronologically from there. however, there is truly no wrong way to
explore.
</p>
<p>
note that there is a blanket trigger warning for everything in this
archive: while there is nothing visually offensive, some portions of
the text and emotions conveyed through this may hit extremely hard.
you are warned.
</p>
<p>
all file dates are real. at least as real as i could figure out.
when i moved data across drives over my years, i accidentally had a
few points where i stamped over all the dates with the day that
moved the files. even fucked it up a final time in february 2025,
while in the process of unfucking things.
</p>
<p>
thankfully, my past self knew i'd want to assemble this kind of
site, and because of that they were crazy about storing the dates of
things inside of html, json/yaml files, and even in fucking
databases. i'm glad it was all stored though, but jeez what a nerd.
</p>
<p>
a few files were touched up for privacy, or otherwise re-encoded.
some of them i added extra metadata.
</p>
<p>
from the bottom of my heart: i hope you enjoy. it has been a
nightmare putting this all together. technically and emotionally
speaking. i'm glad we can put this all behind us, mark it as
completed, and get started with the good shit.
</p>
<p>
love,<br />clo
</p>
<br />
<p>
start here -&gt; <a href="/file/2017">2017</a>
</p>
</div>
</div>
</div>
);
}
export function ForEveryone() {
// deno-fmt-ignore
return <><div class="for_everyone">
<p>today is my 21st birthday. april 30th, 2025.</p>
<p>it's been nearly six months starting hormones.</p>
<p>sometimes i feel great,</p>
<p>sometimes i get dysphoria.</p>
<p>with the walls around me gone</p>
<p>that shit hits way harder than it did before.</p>
<p>ugh..</p>
<p>i'm glad the pain i felt is now explained,</p>
<p>but now rendered in high definition.</p>
<p>the smallest strands of hair on my face and belly act</p>
<p>as sharpened nails to pierce my soul.</p>
<p></p>
<p>it's all a pathway to better days; the sun had risen.</p>
<p>one little step at a time for both of us.</p>
<p>today i quit my job. free falling, it feels so weird.</p>
<p>like sky diving.</p>
<p>the only thing i feel is cold wind.</p>
<p>the only thing i see is everything,</p>
<p>and it's beautiful.</p>
<p>i have a month of falling before the parachute activates,</p>
<p>gonna spend as much time of it on art as i can.</p>
<p>that was, after all, my life plan:</p>
<p>i wanted to make art, all the time,</p>
<p>for everyone.</p>
<p></p>
<p>then you see what happened</p>
<p>to the world and the internet.</p>
<p>i never really got to live through that golden age,</p>
<p>it probably sucked back then too.</p>
<p>but now the big sites definitely stopped being fun.</p>
<p>they slide their cold hands up my body</p>
<p>and feel me around. it's unwelcoming, and</p>
<p>inconsiderate to how sensitive my skin is.</p>
<p>i'm so fucking glad i broke up with YouTube</p>
<p>and their devilish friends.</p>
<p>my NAS is at 5 / 24 TB</p>
<p>and probably wont fill for the next decade.</p>
<p></p>
<p>it took 2 months for me to notice my body changed.</p>
<p>that day was really nice, but it hurt a lot.</p>
<p>a sharp, satisfying pain in my chest gave me life.</p>
<p>learned new instincts for my arms</p>
<p>so they'd stop poking my new shape.</p>
<p>when i look at my face</p>
<p>it's like a different person.</p>
<p>she was the same as before, but completely new.</p>
<p>something changed</p>
<p>or i'm now used to seeing what makes me smile.</p>
<p>regardless, whatever i see in the mirror, i smile.</p>
<p>and, i don't hear that old name much anymore</p>
<p>aside from nightmares. and you'll never repeat it, ok?</p>
<p>okay.</p>
<p></p>
<p>been playing 'new canaan' by 'bill wurtz' on loop</p>
<p>in the background.</p>
<p>it kinda just feels right.</p>
<p>especially when that verse near the end comes on.</p>
<p></p>
<p>more people have been allowed to visit me.</p>
<p>my apartment used to be just for me,</p>
<p>but the more i felt like a person</p>
<p>the more i felt like having others over.</p>
<p>still have to decorate and clean it a little,</p>
<p>but it isn't a job to do alone.</p>
<p>we dragged a giant a rug across the city one day,</p>
<p>and it felt was like anything was possible.</p>
<p>sometimes i have ten people visit in a day,</p>
<p>or sometimes i focus my little eyes on just one.</p>
<p>i never really know what i want to do</p>
<p>until the time actually comes.</p>
<p></p>
{/* FILIP */}
<p>i think about the times i was by the water with you.</p>
<p>the sun setting warmly, icy air fell on our shoulders.</p>
{/* NATALIE */}
<p>and how we walked up to the top of that hill,</p>
<p>you picked up and disposed a nail on the ground,</p>
<p>walking the city thru places i've never been.</p>
{/* BEN */}
<p>or hiking through the park talking about compilers,</p>
<p>tiring me out until i'd fall asleep in your arms.</p>
{/* ELENA */}
<p>and the way you held on to my hand as i woke up,</p>
<p>noticing how i was trying to hide nightmare's tears.</p>
<p></p>
{/* HIGH SCHOOL */}
<p>i remember we were yelling lyrics loudly,</p>
<p>out of key yet cheered on because it was fun.</p>
{/* ADVAITH/NATALIE */}
<p>and when we all toured the big corporate office,</p>
{/* AYU/HARRIS */}
<p>then snuck in to some startup's office after hours;</p>
<p>i don't remember what movie we watched.</p>
{/* COLLEGE, DAY 1 IN EV's ROOM */}
<p>i remember laying on the bunk bed,</p>
<p>while the rest played a card game.</p>
{/* MEGHAN/MORE */}
<p>with us all laying on the rug, staring at the TV</p>
<p>as the ending twist to {/* SEVERANCE */'that show'} was revealed.</p>
<p></p>
<p>all the moments i cherish,</p>
<p>i love because it was always me.</p>
<p>i didn't have to pretend,</p>
<p>even if i didn't know who i was at the time.</p>
<p>you all were there. for me.</p>
<p></p>
<p>i don't want to pretend any more</p>
<p>i want to be myself. for everyone.</p>
<p></p>
<p>oh, the song ended. i thought it was on loop?</p>
<p>it's late... can hear the crickets...</p>
<p>and i can almost see the moon... mmmm...</p>
<p>...nah, too much light pollution.</p>
<p></p>
<p>one day. one day.</p>
<p></p>
<p class="normal">before i go, i want to show the uncensored version of "journal about a girl", because i can trust you at least. keep in mind, i think you're one of the first people to ever see this.</p>
</div>
<div class="for_everyone" style="max-width:80ch;">
<blockquote>
<p>journal - 2024-09-14</p>
<p>been at HackMIT today on behalf of the company. it's fun. me and zack were running around looking for people that might be good hires. he had this magic arbitrary criteria to tell "oh this person is probably cracked let's talk to them" and we go to the first one. they were a nerd, perfect. they seemed to be extremely talented with some extreme software projects.<br/>
okay.. oof... its still clouding my mind<br/>
i cant shake that feeling away</p>
<p>hold on...</p>
<p>at some point they open one of their profiles to navigate to some code, and it displays for a couple of seconds: "pronouns: she/they". i don't actually know anything about this person, but it was my perception that she is trans. their appearance, physique, and age felt similar to me, which tends makes people think you are male.</p>
<p>but... she was having fun being herself. being a legend of identity and of her skill in computer science. winning the physics major. making cool shit at the hackathon, and probably in life. my perception of her was the exact essence of who i myself wanted to be. i was jealous of her life.</p>
<p>i tried hard to avoid a breakdown. success. but i was feeling distant. the next hour or so was disorienting, trying not to think about it too hard. i think there was one possibly interesting person we talked to. i don't remember any of the other conversations. they were not important. but i couldn't think through them regardless.</p>
<p>later, i decided to read some of her code. i either have a huge dislike towards the Rust programming language and/or it was not high quality code. welp, so just is a person studying. my perception was just a perception, inaccurate but impacting. i know i need to become myself, whoever that is. otherwise, i'm just going to feel this shit at higher doses. i think about this every day, and the amount of time i feel being consumed by these problems only grows.</p>
<p>getting through it all is a lonely feeling. not because no one is around, but because i am isolated emotionally. i know other people hit these feelings, but we all are too afraid to speak up, and it's all lonely.</p>
<p>waiting on a reply from someone from healthcare. it'll be slow, but it will be okay.</p>
</blockquote>
</div>
<div class="for_everyone">
<p class="normal">
i've learned that even when i feel alone, it doesn't have to feel lonely. i know it's hard, dear. i know it's scary. but i promise it's possible. we're all in this together. struggling together. sacrificing together. we dedicate our lives to each you, and our art for everyone.
</p>
<p class="normal" style="font-size:2rem;color:#9C91FF;font-family:times,serif;font-style:italic">
and then we knew,<br/>
just like paper airplanes: that we could fly...
</p>
<br />
<p class="normal">
<a href="/" style='text-decoration:underline;text-underline-offset:0.2em;'>fin.</a>
</p>
</div>
</>
}
ForEveryone.class = "text";

264
src/file-viewer/format.ts Normal file
View file

@ -0,0 +1,264 @@
export function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} bytes`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
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 formatDuration(seconds: number) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}
export const escapeUri = (uri: string) =>
encodeURIComponent(uri)
.replace(/%2F/gi, "/")
.replace(/%3A/gi, ":")
.replace(/%2B/gi, "+")
.replace(/%40/gi, "@")
.replace(/%2D/gi, "-")
.replace(/%5F/gi, "_")
.replace(/%2E/gi, ".")
.replace(/%2C/gi, ",");
import type { MediaFile } from "../db.ts";
import { escapeHTML } from "../framework/bun-polyfill.ts";
const findDomain = "paperclover.net";
// Returns escaped HTML
// Features:
// - autolink detection
// - via \bpaperclover.net/[a-zA-Z0-9_\.+-]+
// - via \b/file/[a-zA-Z0-9_\.+-]+
// - via \bhttps://...
// - via name of a sibling file's basename
// - reformat (c) into ©
//
// This formatter was written with AI.
export function highlightLinksInTextView(
text: string,
siblingFiles: MediaFile[] = [],
) {
const siblingLookup = Object.fromEntries(
siblingFiles
.filter((f) => f.basename !== "readme.txt")
.map((f) => [f.basename, f]),
);
// First escape the HTML to prevent XSS
let processedText = escapeHTML(text);
// Replace (c) with ©
processedText = processedText.replace(/\(c\)/gi, "©");
// Process all URL patterns in a single pass to avoid nested links
// This regex matches:
// 1. https:// or http:// URLs
// 2. domain URLs without protocol (e.g., paperclover.net/path)
// 3. /file/ URLs
// 4. ./ relative paths
// We'll use a function to determine what kind of URL it is and format accordingly
const urlRegex = new RegExp(
"(" +
// Group 1: https:// or http:// URLs
"\\bhttps?:\\/\\/[a-zA-Z0-9_\\.\\-]+\\.[a-zA-Z0-9_\\.\\-]+[a-zA-Z0-9_\\.\\-\\/\\?=&%+#]*" +
"|" +
// Group 2: domain URLs without protocol
findDomain +
"\\/\\/[a-zA-Z0-9_\\.\\+\\-]+" +
"|" +
// Group 3: /file/ URLs
"\\/file\\/[a-zA-Z0-9_\\.\\+\\-\\/]+" +
")\\b" +
"|" +
// Group 4: ./ relative paths (not word-bounded)
"(?<=\\s|^)\\.\\/[\\w\\-\\.]+",
"g",
);
processedText = processedText.replace(urlRegex, (match: string) => {
// Case 1: https:// or http:// URLs
if (match.startsWith("http")) {
if (match.includes(findDomain)) {
return `<a href="${
match
.replace(/https?:\/\/paperclover\.net\/+/, "/")
.replace(/\/\/+/g, "/")
}">${match}</a>`;
}
return `<a href="${
match.replace(/\/\/+/g, "/")
}" target="_blank" rel="noopener noreferrer">${match}</a>`;
}
// Case 2: domain URLs without protocol
if (match.startsWith(findDomain)) {
return `<a href="${
match.replace(findDomain + "/", "/").replace(/\/\/+/g, "/")
}">${match}</a>`;
}
// Case 3: /file/ URLs
if (match.startsWith("/file/")) {
return `<a href="${match}">${match}</a>`;
}
// Case 4: ./ relative paths
if (match.startsWith("./")) {
const filename = match.substring(2);
// Check if the filename exists in sibling files
const siblingFile = siblingFiles.find((f) => f.basename === filename);
if (siblingFile) {
return `<a href="/file/${siblingFile.path}">${match}</a>`;
}
// If no exact match but we have sibling files, try to create a reasonable link
if (siblingFiles.length > 0) {
const currentDir = siblingFiles[0].path
.split("/")
.slice(0, -1)
.join("/");
return `<a href="/file/${currentDir}/${filename}">${match}</a>`;
}
}
return match;
});
// Match sibling file names (only if they're not already part of a link)
if (siblingFiles.length > 0) {
// Create a regex pattern that matches any of the sibling file basenames
// We need to escape special regex characters in the filenames
const escapedBasenames = siblingFiles.map((f) =>
f.basename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
);
// Join all basenames with | for the regex alternation
const pattern = new RegExp(`\\b(${escapedBasenames.join("|")})\\b`, "g");
// We need to be careful not to replace text that's already in a link
// So we'll split the text by HTML tags and only process the text parts
const parts = processedText.split(/(<[^>]*>)/);
for (let i = 0; i < parts.length; i += 2) {
// Only process text parts (even indices), not HTML tags (odd indices)
if (i < parts.length) {
parts[i] = parts[i].replace(pattern, (match: string) => {
const file = siblingLookup[match];
if (file) {
return `<a href="/file/${
file.path.replace(/^\//, "").replace(/\/\/+/g, "/")
}">${match}</a>`;
}
return match;
});
}
}
processedText = parts.join("");
}
return processedText;
}
export function highlightConvo(text: string) {
text = text.replace(/^#mode=convo\n/, "");
const lines = text.split("\n");
const paras: { speaker: string | null; lines: string[] }[] = [];
let currentPara: string[] = [];
let currentSpeaker: string | null = null;
let firstSpeaker = null;
const speakers: Record<string, string> = {};
const getSpeaker = (s: string) => {
if (s[1] === " " && speakers[s[0]]) {
return s[0];
}
return null;
};
for (const line of lines) {
let trimmed = line.trim();
if (line.startsWith("#")) {
// parse #X=Y
const [_, speaker, color] = trimmed.match(/^#(.)=(.*)$/)!;
speakers[speaker] = color;
continue;
}
if (trimmed === "") {
continue;
}
let speaker = getSpeaker(trimmed);
if (speaker) {
trimmed = trimmed.substring(speaker.length).trimStart();
speaker = speakers[speaker];
} else {
speaker = "me";
}
trimmed = trimmed.replace(
/\[IMG:(\/file\/[^\]]+)\]/g,
'<img src="$1" alt="attachment" class="convo-img" width="300" />',
);
if (trimmed === "---" && speaker === "me") {
trimmed = "<hr/>";
}
if (speaker === currentSpeaker) {
currentPara.push(trimmed);
} else {
if (currentPara.length > 0) {
paras.push({
speaker: currentSpeaker,
lines: currentPara,
});
currentPara = [];
}
currentPara = [trimmed];
currentSpeaker = speaker;
firstSpeaker ??= speaker;
}
}
if (currentPara.length > 0) {
paras.push({
speaker: currentSpeaker,
lines: currentPara,
});
}
return paras
.map(({ speaker, lines }) => {
return `<div class="s-${speaker}">${
lines
.map((line) => `<div class="line">${line}</div>`)
.join("\n")
}</div>`;
})
.join("\n");
}
export function highlightHashComments(text: string) {
const lines = text.split("\n");
return lines
.map((line) => {
if (line.startsWith("#")) {
return `<div style="color: var(--primary);">${line}</div>`;
}
return `<div>${line.trimEnd() || "&nbsp;"}</div>`;
})
.join("\n");
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,268 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>patch</string>
<string>diff</string>
<string>rej</string>
</array>
<key>firstLineMatch</key>
<string>(?x)^
(===\ modified\ file
|==== \s* // .+ \s - \s .+ \s+ ====
|Index:\
|---\ [^%\n]
|\*\*\*.*\d{4}\s*$
|\d+(,\d+)* (a|d|c) \d+(,\d+)* $
|diff\ --git\
|commit\ [0-9a-f]{40}$
)</string>
<key>keyEquivalent</key>
<string>^~D</string>
<key>name</key>
<string>Diff</string>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.separator.diff</string>
</dict>
</dict>
<key>match</key>
<string>^((\*{15})|(={67})|(-{3}))$\n?</string>
<key>name</key>
<string>meta.separator.diff</string>
</dict>
<dict>
<key>match</key>
<string>^\d+(,\d+)*(a|d|c)\d+(,\d+)*$\n?</string>
<key>name</key>
<string>meta.diff.range.normal</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>meta.toc-list.line-number.diff</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
</dict>
<key>match</key>
<string>^(@@)\s*(.+?)\s*(@@.*)($\n?)?</string>
<key>name</key>
<string>meta.diff.range.unified</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
<key>7</key>
<dict>
<key>name</key>
<string>punctuation.definition.range.diff</string>
</dict>
</dict>
<key>match</key>
<string>^(((\-{3}) .+ (\-{4}))|((\*{3}) .+ (\*{4})))$\n?</string>
<key>name</key>
<string>meta.diff.range.context</string>
</dict>
<dict>
<key>match</key>
<string>^diff --git a/.*$\n?</string>
<key>name</key>
<string>meta.diff.header.git</string>
</dict>
<dict>
<key>match</key>
<string>^diff (-|\S+\s+\S+).*$\n?</string>
<key>name</key>
<string>meta.diff.header.command</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.definition.from-file.diff</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>punctuation.definition.from-file.diff</string>
</dict>
<key>7</key>
<dict>
<key>name</key>
<string>punctuation.definition.from-file.diff</string>
</dict>
</dict>
<key>match</key>
<string>(^(((-{3}) .+)|((\*{3}) .+))$\n?|^(={4}) .+(?= - ))</string>
<key>name</key>
<string>meta.diff.header.from-file</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.definition.to-file.diff</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.to-file.diff</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.definition.to-file.diff</string>
</dict>
</dict>
<key>match</key>
<string>(^(\+{3}) .+$\n?| (-) .* (={4})$\n?)</string>
<key>name</key>
<string>meta.diff.header.to-file</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.inserted.diff</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>punctuation.definition.inserted.diff</string>
</dict>
</dict>
<key>match</key>
<string>^(((&gt;)( .*)?)|((\+).*))$\n?</string>
<key>name</key>
<string>markup.inserted.diff</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.changed.diff</string>
</dict>
</dict>
<key>match</key>
<string>^(!).*$\n?</string>
<key>name</key>
<string>markup.changed.diff</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.deleted.diff</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>punctuation.definition.deleted.diff</string>
</dict>
</dict>
<key>match</key>
<string>^(((&lt;)( .*)?)|((-).*))$\n?</string>
<key>name</key>
<string>markup.deleted.diff</string>
</dict>
<dict>
<key>begin</key>
<string>^(#)</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.diff</string>
</dict>
</dict>
<key>comment</key>
<string>Git produces unified diffs with embedded comments"</string>
<key>end</key>
<string>\n</string>
<key>name</key>
<string>comment.line.number-sign.diff</string>
</dict>
<dict>
<key>match</key>
<string>^index [0-9a-f]{7,40}\.\.[0-9a-f]{7,40}.*$\n?</string>
<key>name</key>
<string>meta.diff.index.git</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.separator.key-value.diff</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>meta.toc-list.file-name.diff</string>
</dict>
</dict>
<key>match</key>
<string>^Index(:) (.+)$\n?</string>
<key>name</key>
<string>meta.diff.index</string>
</dict>
<dict>
<key>match</key>
<string>^Only in .*: .*$\n?</string>
<key>name</key>
<string>meta.diff.only-in</string>
</dict>
</array>
<key>scopeName</key>
<string>source.diff</string>
<key>uuid</key>
<string>7E848FF4-708E-11D9-97B4-0011242E4184</string>
</dict>
</plist>

View file

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>uuid</key>
<string>E07EC438-7B75-4437-8AA1-DA94C1E6EACC</string>
<key>patterns</key>
<array>
<dict>
<key>name</key>
<string>keyword.command.dosbatch</string>
<key>match</key>
<string>\b(?i)(?:append|assoc|at|attrib|break|cacls|cd|chcp|chdir|chkdsk|chkntfs|cls|cmd|color|comp|compact|convert|copy|date|del|dir|diskcomp|diskcopy|doskey|echo|endlocal|erase|fc|find|findstr|format|ftype|graftabl|help|keyb|label|md|mkdir|mode|more|move|path|pause|popd|print|prompt|pushd|rd|recover|ren|rename|replace|restore|rmdir|set|setlocal|shift|sort|start|subst|time|title|tree|type|ver|verify|vol|xcopy)\b</string>
</dict>
<dict>
<key>name</key>
<string>keyword.control.statement.dosbatch</string>
<key>match</key>
<string>\b(?i)(?:goto|call|exit)\b</string>
</dict>
<dict>
<key>name</key>
<string>keyword.control.conditional.if.dosbatch</string>
<key>match</key>
<string>\b(?i)if\s+((not)\s+)(exist|defined|errorlevel|cmdextversion)\b</string>
</dict>
<dict>
<key>name</key>
<string>keyword.control.conditional.dosbatch</string>
<key>match</key>
<string>\b(?i)(?:if|else)\b</string>
</dict>
<dict>
<key>name</key>
<string>keyword.control.repeat.dosbatch</string>
<key>match</key>
<string>\b(?i)for\b</string>
</dict>
<dict>
<key>name</key>
<string>keyword.operator.dosbatch</string>
<key>match</key>
<string>\b(?:EQU|NEQ|LSS|LEQ|GTR|GEQ)\b</string>
</dict>
<dict>
<key>name</key>
<string>comment.line.rem.dosbatch</string>
<key>match</key>
<string>\b(?i)rem(?:$|\s.*$)</string>
</dict>
<dict>
<key>name</key>
<string>comment.line.colons.dosbatch</string>
<key>match</key>
<string>\s*:\s*:.*$</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.parameter.function.begin.shell</string>
</dict>
</dict>
<key>name</key>
<string>variable.parameter.function.dosbatch</string>
<key>match</key>
<string>(?i)(%)(~(?:f|d|p|n|x|s|a|t|z|\$[^:]*:)*)?\d</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.parameter.loop.begin.shell</string>
</dict>
</dict>
<key>name</key>
<string>variable.parameter.loop.dosbatch</string>
<key>match</key>
<string>(?i)(%%)(~(?:f|d|p|n|x|s|a|t|z|\$[^:]*:)*)?[a-z]</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.parsetime.begin.shell</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>variable.other.parsetime.end.shell</string>
</dict>
</dict>
<key>name</key>
<string>variable.other.parsetime.dosbatch</string>
<key>match</key>
<string>(%)[^%]+(%)</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.delayed.begin.shell</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>variable.other.delayed.end.shell</string>
</dict>
</dict>
<key>name</key>
<string>variable.other.delayed.dosbatch</string>
<key>match</key>
<string>(!)[^!]+(!)</string>
</dict>
<dict>
<key>begin</key>
<string>"</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.shell</string>
</dict>
</dict>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.shell</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.double.dosbatch</string>
<key>end</key>
<string>"|$</string>
</dict>
<dict>
<key>name</key>
<string>keyword.operator.pipe.dosbatch</string>
<key>match</key>
<string>[|]</string>
</dict>
<dict>
<key>name</key>
<string>keyword.operator.redirect.shell</string>
<key>match</key>
<string>&amp;&gt;|\d*&gt;&amp;\d*|\d*(&gt;&gt;|&gt;|&lt;)|\d*&lt;&amp;|\d*&lt;&gt;</string>
</dict>
</array>
<key>name</key>
<string>Batch File</string>
<key>scopeName</key>
<string>source.dosbatch</string>
<key>fileTypes</key>
<array>
<string>bat</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,386 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>json</string>
<string>sublime-settings</string>
<string>sublime-menu</string>
<string>sublime-keymap</string>
<string>sublime-mousemap</string>
<string>sublime-theme</string>
<string>sublime-build</string>
<string>sublime-project</string>
<string>sublime-completions</string>
</array>
<key>foldingStartMarker</key>
<string>(?x) # turn on extended mode
^ # a line beginning with
\s* # some optional space
[{\[] # the start of an object or array
(?! # but not followed by
.* # whatever
[}\]] # and the close of an object or array
,? # an optional comma
\s* # some optional space
$ # at the end of the line
)
| # ...or...
[{\[] # the start of an object or array
\s* # some optional space
$ # at the end of the line</string>
<key>foldingStopMarker</key>
<string>(?x) # turn on extended mode
^ # a line beginning with
\s* # some optional space
[}\]] # and the close of an object or array</string>
<key>keyEquivalent</key>
<string>^~J</string>
<key>name</key>
<string>JSON (Javascript Next)</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#value</string>
</dict>
</array>
<key>repository</key>
<dict>
<key>array</key>
<dict>
<key>begin</key>
<string>\[</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.array.begin.json</string>
</dict>
</dict>
<key>end</key>
<string>\]</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.array.end.json</string>
</dict>
</dict>
<key>name</key>
<string>meta.structure.array.json</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#value</string>
</dict>
<dict>
<key>match</key>
<string>,</string>
<key>name</key>
<string>punctuation.separator.array.json</string>
</dict>
<dict>
<key>match</key>
<string>[^\s\]]</string>
<key>name</key>
<string>invalid.illegal.expected-array-separator.json</string>
</dict>
</array>
</dict>
<key>comments</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>/\*\*(?!/)</string>
<key>captures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.json</string>
</dict>
</dict>
<key>end</key>
<string>\*/</string>
<key>name</key>
<string>comment.block.documentation.json</string>
</dict>
<dict>
<key>begin</key>
<string>/\*</string>
<key>captures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.json</string>
</dict>
</dict>
<key>end</key>
<string>\*/</string>
<key>name</key>
<string>comment.block.json</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.json</string>
</dict>
</dict>
<key>match</key>
<string>(//).*$\n?</string>
<key>name</key>
<string>comment.line.double-slash.js</string>
</dict>
</array>
</dict>
<key>constant</key>
<dict>
<key>match</key>
<string>\b(?:true|false|null)\b</string>
<key>name</key>
<string>constant.language.json</string>
</dict>
<key>number</key>
<dict>
<key>match</key>
<string>(?x) # turn on extended mode
-? # an optional minus
(?:
0 # a zero
| # ...or...
[1-9] # a 1-9 character
\d* # followed by zero or more digits
)
(?:
(?:
\. # a period
\d+ # followed by one or more digits
)?
(?:
[eE] # an e character
[+-]? # followed by an option +/-
\d+ # followed by one or more digits
)? # make exponent optional
)? # make decimal portion optional</string>
<key>name</key>
<string>constant.numeric.json</string>
</dict>
<key>object</key>
<dict>
<key>begin</key>
<string>\{</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.dictionary.begin.json</string>
</dict>
</dict>
<key>end</key>
<string>\}</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.dictionary.end.json</string>
</dict>
</dict>
<key>name</key>
<string>meta.structure.dictionary.json</string>
<key>patterns</key>
<array>
<dict>
<key>comment</key>
<string>the JSON object key</string>
<key>include</key>
<string>#objectkey</string>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
<dict>
<key>begin</key>
<string>:</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.separator.dictionary.key-value.json</string>
</dict>
</dict>
<key>end</key>
<string>(,)|(?=\})</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.separator.dictionary.pair.json</string>
</dict>
</dict>
<key>name</key>
<string>meta.structure.dictionary.value.json</string>
<key>patterns</key>
<array>
<dict>
<key>comment</key>
<string>the JSON object value</string>
<key>include</key>
<string>#value</string>
</dict>
<dict>
<key>match</key>
<string>[^\s,]</string>
<key>name</key>
<string>invalid.illegal.expected-dictionary-separator.json</string>
</dict>
</array>
</dict>
<dict>
<key>match</key>
<string>[^\s\}]</string>
<key>name</key>
<string>invalid.illegal.expected-dictionary-separator.json</string>
</dict>
</array>
</dict>
<key>string</key>
<dict>
<key>begin</key>
<string>"</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.json</string>
</dict>
</dict>
<key>end</key>
<string>"</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.json</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.double.json</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#stringcontent</string>
</dict>
</array>
</dict>
<key>objectkey</key>
<dict>
<key>begin</key>
<string>"</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.support.type.property-name.begin.json</string>
</dict>
</dict>
<key>end</key>
<string>"</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.support.type.property-name.end.json</string>
</dict>
</dict>
<key>name</key>
<string>string.json support.type.property-name.json</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#stringcontent</string>
</dict>
</array>
</dict>
<key>stringcontent</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>(?x) # turn on extended mode
\\ # a literal backslash
(?: # ...followed by...
["\\/bfnrt] # one of these characters
| # ...or...
u # a u
[0-9a-fA-F]{4}) # and four hex digits</string>
<key>name</key>
<string>constant.character.escape.json</string>
</dict>
<dict>
<key>match</key>
<string>\\.</string>
<key>name</key>
<string>invalid.illegal.unrecognized-string-escape.json</string>
</dict>
</array>
</dict>
<key>value</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#constant</string>
</dict>
<dict>
<key>include</key>
<string>#number</string>
</dict>
<dict>
<key>include</key>
<string>#string</string>
</dict>
<dict>
<key>include</key>
<string>#array</string>
</dict>
<dict>
<key>include</key>
<string>#object</string>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
</array>
</dict>
</dict>
<key>scopeName</key>
<string>source.json</string>
<key>uuid</key>
<string>8f97457b-516e-48ce-83c7-08ae12fb327a</string>
</dict>
</plist>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,736 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>toml</string>
</array>
<key>keyEquivalent</key>
<string>^~T</string>
<key>name</key>
<string>TOML</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
<dict>
<key>include</key>
<string>#groups</string>
</dict>
<dict>
<key>include</key>
<string>#key_pair</string>
</dict>
<dict>
<key>include</key>
<string>#invalid</string>
</dict>
</array>
<key>repository</key>
<dict>
<key>comments</key>
<dict>
<key>begin</key>
<string>(^[ \t]+)?(?=#)</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.whitespace.comment.leading.toml</string>
</dict>
</dict>
<key>end</key>
<string>(?!\G)</string>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>#</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.toml</string>
</dict>
</dict>
<key>end</key>
<string>\n</string>
<key>name</key>
<string>comment.line.number-sign.toml</string>
</dict>
</array>
</dict>
<key>groups</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.section.begin.toml</string>
</dict>
<key>2</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>[^\s.]+</string>
<key>name</key>
<string>entity.name.section.toml</string>
</dict>
</array>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.section.begin.toml</string>
</dict>
</dict>
<key>match</key>
<string>^\s*(\[)([^\[\]]*)(\])</string>
<key>name</key>
<string>meta.group.toml</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.section.begin.toml</string>
</dict>
<key>2</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>[^\s.]+</string>
<key>name</key>
<string>entity.name.section.toml</string>
</dict>
</array>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.section.begin.toml</string>
</dict>
</dict>
<key>match</key>
<string>^\s*(\[\[)([^\[\]]*)(\]\])</string>
<key>name</key>
<string>meta.group.double.toml</string>
</dict>
</array>
</dict>
<key>invalid</key>
<dict>
<key>match</key>
<string>\S+(\s*(?=\S))?</string>
<key>name</key>
<string>invalid.illegal.not-allowed-here.toml</string>
</dict>
<key>key_pair</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>([A-Za-z0-9_-]+)\s*(=)\s*</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.key.toml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.separator.key-value.toml</string>
</dict>
</dict>
<key>end</key>
<string>(?&lt;=\S)(?&lt;!=)|$</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#primatives</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>((")(.*?)("))\s*(=)\s*</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.key.toml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.begin.toml</string>
</dict>
<key>3</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
<key>name</key>
<string>constant.character.escape.toml</string>
</dict>
<dict>
<key>match</key>
<string>\\[^btnfr"\\]</string>
<key>name</key>
<string>invalid.illegal.escape.toml</string>
</dict>
<dict>
<key>match</key>
<string>"</string>
<key>name</key>
<string>invalid.illegal.not-allowed-here.toml</string>
</dict>
</array>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.end.toml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>punctuation.separator.key-value.toml</string>
</dict>
</dict>
<key>end</key>
<string>(?&lt;=\S)(?&lt;!=)|$</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#primatives</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>((')([^']*)('))\s*(=)\s*</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.key.toml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.begin.toml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.end.toml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>punctuation.separator.key-value.toml</string>
</dict>
</dict>
<key>end</key>
<string>(?&lt;=\S)(?&lt;!=)|$</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#primatives</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>(?x)
(
(
(?:
[A-Za-z0-9_-]+ # Bare key
| " (?:[^"\\]|\\.)* " # Double quoted key
| ' [^']* ' # Sindle quoted key
)
(?:
\s* \. \s* # Dot
| (?= \s* =) # or look-ahead for equals
)
){2,} # Ensure at least one dot
)
\s*(=)\s*
</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.key.toml</string>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\.</string>
<key>name</key>
<string>punctuation.separator.variable.toml</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.begin.toml</string>
</dict>
<key>2</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
<key>name</key>
<string>constant.character.escape.toml</string>
</dict>
<dict>
<key>match</key>
<string>\\[^btnfr"\\]</string>
<key>name</key>
<string>invalid.illegal.escape.toml</string>
</dict>
</array>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.end.toml</string>
</dict>
</dict>
<key>match</key>
<string>(")((?:[^"\\]|\\.)*)(")</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.begin.toml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.definition.variable.end.toml</string>
</dict>
</dict>
<key>match</key>
<string>(')[^']*(')</string>
</dict>
</array>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.separator.key-value.toml</string>
</dict>
</dict>
<key>comment</key>
<string>Dotted key</string>
<key>end</key>
<string>(?&lt;=\S)(?&lt;!=)|$</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#primatives</string>
</dict>
</array>
</dict>
</array>
</dict>
<key>primatives</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>\G"""</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>"{3,5}</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.triple.double.toml</string>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
<key>name</key>
<string>constant.character.escape.toml</string>
</dict>
<dict>
<key>match</key>
<string>\\[^btnfr"\\\n]</string>
<key>name</key>
<string>invalid.illegal.escape.toml</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>\G"</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>"</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.double.toml</string>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
<key>name</key>
<string>constant.character.escape.toml</string>
</dict>
<dict>
<key>match</key>
<string>\\[^btnfr"\\]</string>
<key>name</key>
<string>invalid.illegal.escape.toml</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>\G'''</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>'{3,5}</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.triple.single.toml</string>
</dict>
<dict>
<key>begin</key>
<string>\G'</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>'</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.single.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G(?x)
[0-9]{4}
-
(0[1-9]|1[012])
-
(?!00|3[2-9])[0-3][0-9]
(
[Tt ]
(?!2[5-9])[0-2][0-9]
:
[0-5][0-9]
:
(?!6[1-9])[0-6][0-9]
(\.[0-9]+)?
(
Z
| [+-](?!2[5-9])[0-2][0-9]:[0-5][0-9]
)?
)?
</string>
<key>name</key>
<string>constant.other.date.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G(?x)
(?!2[5-9])[0-2][0-9]
:
[0-5][0-9]
:
(?!6[1-9])[0-6][0-9]
(\.[0-9]+)?
</string>
<key>name</key>
<string>constant.other.time.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G(true|false)</string>
<key>name</key>
<string>constant.language.boolean.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G0x\h(\h|_\h)*</string>
<key>name</key>
<string>constant.numeric.hex.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G0o[0-7]([0-7]|_[0-7])*</string>
<key>name</key>
<string>constant.numeric.octal.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G0b[01]([01]|_[01])*</string>
<key>name</key>
<string>constant.numeric.binary.toml</string>
</dict>
<dict>
<key>match</key>
<string>\G[+-]?(inf|nan)</string>
<key>name</key>
<string>constant.numeric.toml</string>
</dict>
<dict>
<key>match</key>
<string>(?x)
\G
(
[+-]?
(
0
| ([1-9](([0-9]|_[0-9])+)?)
)
)
(?=[.eE])
(
\.
([0-9](([0-9]|_[0-9])+)?)
)?
(
[eE]
([+-]?[0-9](([0-9]|_[0-9])+)?)
)?
</string>
<key>name</key>
<string>constant.numeric.float.toml</string>
</dict>
<dict>
<key>match</key>
<string>(?x)
\G
(
[+-]?
(
0
| ([1-9](([0-9]|_[0-9])+)?)
)
)
</string>
<key>name</key>
<string>constant.numeric.integer.toml</string>
</dict>
<dict>
<key>begin</key>
<string>\G\[</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.array.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>\]</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.array.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>meta.array.toml</string>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>(?=["'']|[+-]?[0-9]|[+-]?(inf|nan)|true|false|\[|\{)</string>
<key>end</key>
<string>,|(?=])</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.separator.array.toml</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#primatives</string>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
<dict>
<key>include</key>
<string>#invalid</string>
</dict>
</array>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
<dict>
<key>include</key>
<string>#invalid</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>\G\{</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.inline-table.begin.toml</string>
</dict>
</dict>
<key>end</key>
<string>\}</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.inline-table.end.toml</string>
</dict>
</dict>
<key>name</key>
<string>meta.inline-table.toml</string>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>(?=\S)</string>
<key>end</key>
<string>,|(?=})</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.separator.inline-table.toml</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#key_pair</string>
</dict>
</array>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
</array>
</dict>
</array>
</dict>
</dict>
<key>scopeName</key>
<string>source.toml</string>
<key>uuid</key>
<string>7DEF2EDB-5BB7-4DD2-9E78-3541A26B7923</string>
</dict>
</plist>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,573 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>xml</string>
<string>xsd</string>
<string>tld</string>
<string>jsp</string>
<string>pt</string>
<string>cpt</string>
<string>dtml</string>
<string>rss</string>
<string>opml</string>
</array>
<key>keyEquivalent</key>
<string>^~X</string>
<key>name</key>
<string>XML</string>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>(&lt;\?)\s*([-_a-zA-Z0-9]+)</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.tag.xml</string>
</dict>
</dict>
<key>end</key>
<string>(\?&gt;)</string>
<key>name</key>
<string>meta.tag.metadata.processing.xml</string>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string> ([a-zA-Z-]+)</string>
<key>name</key>
<string>entity.other.attribute-name.xml</string>
</dict>
<dict>
<key>include</key>
<string>#doublequotedString</string>
</dict>
<dict>
<key>include</key>
<string>#singlequotedString</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>(&lt;!)(DOCTYPE)\s+([:a-zA-Z_][:a-zA-Z0-9_.-]*)</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.tag.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.other.attribute-name.documentroot.xml</string>
</dict>
</dict>
<key>end</key>
<string>\s*(&gt;)</string>
<key>name</key>
<string>meta.tag.metadata.doctype.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#internalSubset</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>&lt;[!%]--</string>
<key>captures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.comment.xml</string>
</dict>
</dict>
<key>end</key>
<string>--%?&gt;</string>
<key>name</key>
<string>comment.block.xml</string>
</dict>
<dict>
<key>begin</key>
<string>(&lt;)((?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+))(?=(\s[^&gt;]*)?&gt;&lt;/\2&gt;)</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.name.tag.namespace.xml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>entity.name.tag.xml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>punctuation.separator.namespace.xml</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>entity.name.tag.localname.xml</string>
</dict>
</dict>
<key>end</key>
<string>(&gt;(&lt;))/(?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+)(&gt;)</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>meta.scope.between-tag-pair.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.name.tag.namespace.xml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>entity.name.tag.xml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>punctuation.separator.namespace.xml</string>
</dict>
<key>6</key>
<dict>
<key>name</key>
<string>entity.name.tag.localname.xml</string>
</dict>
<key>7</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
</dict>
<key>name</key>
<string>meta.tag.no-content.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#tagStuff</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>(&lt;/?)(?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+)</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.tag.namespace.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.name.tag.xml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>punctuation.separator.namespace.xml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>entity.name.tag.localname.xml</string>
</dict>
</dict>
<key>end</key>
<string>(/?&gt;)</string>
<key>name</key>
<string>meta.tag.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#tagStuff</string>
</dict>
</array>
</dict>
<dict>
<key>include</key>
<string>#entity</string>
</dict>
<dict>
<key>include</key>
<string>#bare-ampersand</string>
</dict>
<dict>
<key>begin</key>
<string>&lt;%@</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.section.embedded.begin.xml</string>
</dict>
</dict>
<key>end</key>
<string>%&gt;</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.section.embedded.end.xml</string>
</dict>
</dict>
<key>name</key>
<string>source.java-props.embedded.xml</string>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>page|include|taglib</string>
<key>name</key>
<string>keyword.other.page-props.xml</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>&lt;%[!=]?(?!--)</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.section.embedded.begin.xml</string>
</dict>
</dict>
<key>end</key>
<string>(?!--)%&gt;</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.section.embedded.end.xml</string>
</dict>
</dict>
<key>name</key>
<string>source.java.embedded.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>source.java</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>&lt;!\[CDATA\[</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.xml</string>
</dict>
</dict>
<key>end</key>
<string>]]&gt;</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.xml</string>
</dict>
</dict>
<key>name</key>
<string>string.unquoted.cdata.xml</string>
</dict>
</array>
<key>repository</key>
<dict>
<key>EntityDecl</key>
<dict>
<key>begin</key>
<string>(&lt;!)(ENTITY)\s+(%\s+)?([:a-zA-Z_][:a-zA-Z0-9_.-]*)(\s+(?:SYSTEM|PUBLIC)\s+)?</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.tag.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>keyword.other.entity.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.entity.xml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>variable.language.entity.xml</string>
</dict>
<key>5</key>
<dict>
<key>name</key>
<string>keyword.other.entitytype.xml</string>
</dict>
</dict>
<key>end</key>
<string>(&gt;)</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#doublequotedString</string>
</dict>
<dict>
<key>include</key>
<string>#singlequotedString</string>
</dict>
</array>
</dict>
<key>bare-ampersand</key>
<dict>
<key>match</key>
<string>&amp;</string>
<key>name</key>
<string>invalid.illegal.bad-ampersand.xml</string>
</dict>
<key>doublequotedString</key>
<dict>
<key>begin</key>
<string>"</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.xml</string>
</dict>
</dict>
<key>end</key>
<string>"</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.xml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.double.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#entity</string>
</dict>
<dict>
<key>include</key>
<string>#bare-ampersand</string>
</dict>
</array>
</dict>
<key>entity</key>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.constant.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.constant.xml</string>
</dict>
</dict>
<key>match</key>
<string>(&amp;)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)</string>
<key>name</key>
<string>constant.character.entity.xml</string>
</dict>
<key>internalSubset</key>
<dict>
<key>begin</key>
<string>(\[)</string>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.constant.xml</string>
</dict>
</dict>
<key>end</key>
<string>(\])</string>
<key>name</key>
<string>meta.internalsubset.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#EntityDecl</string>
</dict>
<dict>
<key>include</key>
<string>#parameterEntity</string>
</dict>
</array>
</dict>
<key>parameterEntity</key>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.definition.constant.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.definition.constant.xml</string>
</dict>
</dict>
<key>match</key>
<string>(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)</string>
<key>name</key>
<string>constant.character.parameter-entity.xml</string>
</dict>
<key>singlequotedString</key>
<dict>
<key>begin</key>
<string>'</string>
<key>beginCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.begin.xml</string>
</dict>
</dict>
<key>end</key>
<string>'</string>
<key>endCaptures</key>
<dict>
<key>0</key>
<dict>
<key>name</key>
<string>punctuation.definition.string.end.xml</string>
</dict>
</dict>
<key>name</key>
<string>string.quoted.single.xml</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#entity</string>
</dict>
<dict>
<key>include</key>
<string>#bare-ampersand</string>
</dict>
</array>
</dict>
<key>tagStuff</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>entity.other.attribute-name.namespace.xml</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.other.attribute-name.xml</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.separator.namespace.xml</string>
</dict>
<key>4</key>
<dict>
<key>name</key>
<string>entity.other.attribute-name.localname.xml</string>
</dict>
</dict>
<key>match</key>
<string> (?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9]+)=</string>
</dict>
<dict>
<key>include</key>
<string>#doublequotedString</string>
</dict>
<dict>
<key>include</key>
<string>#singlequotedString</string>
</dict>
</array>
</dict>
</dict>
<key>scopeName</key>
<string>text.xml</string>
<key>uuid</key>
<string>D3C4E6DA-6B1C-11D9-8CC2-000D93589AF6</string>
</dict>
</plist>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,846 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>zig</string>
</array>
<key>keyEquivalent</key>
<string>^~Z</string>
<key>name</key>
<string>Zig</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
</array>
<key>repository</key>
<dict>
<key>block</key>
<dict>
<key>begin</key>
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(\{)</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.section.braces.begin.zig</string>
</dict>
</dict>
<key>end</key>
<string>(\})</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>punctuation.section.braces.end.zig</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
</array>
</dict>
<key>character_escapes</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\\n</string>
<key>name</key>
<string>constant.character.escape.newline.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\r</string>
<key>name</key>
<string>constant.character.escape.carrigereturn.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\t</string>
<key>name</key>
<string>constant.character.escape.tabulator.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\\\</string>
<key>name</key>
<string>constant.character.escape.backslash.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\'</string>
<key>name</key>
<string>constant.character.escape.single-quote.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\\"</string>
<key>name</key>
<string>constant.character.escape.double-quote.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\x[a-fA-F\d]{2}</string>
<key>name</key>
<string>constant.character.escape.hexidecimal.zig</string>
</dict>
<dict>
<key>match</key>
<string>\\u\{[a-fA-F\d]{1,6}\}</string>
<key>name</key>
<string>constant.character.escape.hexidecimal.zig</string>
</dict>
</array>
</dict>
<key>comments</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>///</string>
<key>end</key>
<string>$\n?</string>
<key>name</key>
<string>comment.line.documentation.zig</string>
</dict>
<dict>
<key>begin</key>
<string>//[^/]\s*TODO</string>
<key>end</key>
<string>$\n?</string>
<key>name</key>
<string>comment.line.todo.zig</string>
</dict>
<dict>
<key>begin</key>
<string>//[^/]*</string>
<key>end</key>
<string>$\n?</string>
<key>name</key>
<string>comment.line.zig</string>
</dict>
</array>
</dict>
<key>constants</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\b(null|undefined|true|false)\b</string>
<key>name</key>
<string>constant.language.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?&lt;!\.)(-?[\d_]+)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.integer.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?&lt;!\.)(0x[a-fA-F\d_]+)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.integer.hexadecimal.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?&lt;!\.)(0o[0-7_]+)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.integer.octal.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?&lt;!\.)(0b[01_]+)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.integer.binary.zig</string>
</dict>
<dict>
<key>match</key>
<string>(?&lt;!\.)(-?\b[\d_]+(?:\.[\d_]+)?(?:[eE][+-]?[\d_]+)?)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.float.zig</string>
</dict>
<dict>
<key>match</key>
<string>(?&lt;!\.)(-?\b0x[a-fA-F\d_]+(?:\.[a-fA-F\d_]+)?[pP]?(?:[+-]?[\d_]+)?)(?!\.)\b</string>
<key>name</key>
<string>constant.numeric.float.hexadecimal.zig</string>
</dict>
</array>
</dict>
<key>container_decl</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:union)\s*[(\{])</string>
<key>name</key>
<string>entity.name.union.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:struct)\s*[(\{])</string>
<key>name</key>
<string>entity.name.struct.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:enum)\s*[(\{])</string>
<key>name</key>
<string>entity.name.enum.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:error)\s*[(\{])</string>
<key>name</key>
<string>entity.name.error.zig</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.error.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.accessor.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.name.error.zig</string>
</dict>
</dict>
<key>match</key>
<string>\b(error)(\.)([a-zA-Z_]\w*|@\".+\")</string>
</dict>
</array>
</dict>
<key>dummy_main</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#label</string>
</dict>
<dict>
<key>include</key>
<string>#function_type</string>
</dict>
<dict>
<key>include</key>
<string>#punctuation</string>
</dict>
<dict>
<key>include</key>
<string>#storage_modifier</string>
</dict>
<dict>
<key>include</key>
<string>#container_decl</string>
</dict>
<dict>
<key>include</key>
<string>#constants</string>
</dict>
<dict>
<key>include</key>
<string>#comments</string>
</dict>
<dict>
<key>include</key>
<string>#strings</string>
</dict>
<dict>
<key>include</key>
<string>#storage</string>
</dict>
<dict>
<key>include</key>
<string>#keywords</string>
</dict>
<dict>
<key>include</key>
<string>#operators</string>
</dict>
<dict>
<key>include</key>
<string>#support</string>
</dict>
<dict>
<key>include</key>
<string>#field_decl</string>
</dict>
<dict>
<key>include</key>
<string>#block</string>
</dict>
<dict>
<key>include</key>
<string>#function_def</string>
</dict>
<dict>
<key>include</key>
<string>#function_call</string>
</dict>
<dict>
<key>include</key>
<string>#enum_literal</string>
</dict>
</array>
</dict>
<key>enum_literal</key>
<dict>
<key>match</key>
<string>(?&lt;!\w|\)|\?|\}|\]|\*)(\.(?:[a-zA-Z_]\w*\b|@\"[^\"]*\"))(?!\(|\s*=[^=&gt;])</string>
<key>name</key>
<string>constant.language.enum</string>
</dict>
<key>field_decl</key>
<dict>
<key>begin</key>
<string>([a-zA-Z_]\w*|@\".+\")\s*(:)\s*</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.other.member.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.separator.zig</string>
</dict>
</dict>
<key>end</key>
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(?:(,)|(=)|$)</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.separator.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>keyword.operator.assignment.zig</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
</array>
</dict>
<key>function_call</key>
<dict>
<key>match</key>
<string>(?&lt;!fn)\b([a-zA-Z_]\w*|@\".+\")(?=\s*\()</string>
<key>name</key>
<string>variable.function.zig</string>
</dict>
<key>function_def</key>
<dict>
<key>begin</key>
<string>(?&lt;=fn)\s+([a-zA-Z_]\w*|@\".+\")(\()</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>entity.name.function</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.section.parens.begin.zig</string>
</dict>
</dict>
<key>end</key>
<string>(?&lt;=\)[^\)])\s*([a-zA-Z_][\w.]*|@\".+\")?(!)?\s*(?:([a-zA-Z_][\w.]*|@\".+\")\b(?!\s*\())?</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>keyword.operator.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#label</string>
</dict>
<dict>
<key>include</key>
<string>#param_list</string>
</dict>
<dict>
<key>match</key>
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
</array>
</dict>
<key>function_type</key>
<dict>
<key>begin</key>
<string>\b(fn)\s*(\()</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.function.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.section.parens.begin.zig</string>
</dict>
</dict>
<key>contentName</key>
<string>meta.function.parameters.zig</string>
<key>end</key>
<string>(?&lt;=\)|\})\s*([a-zA-Z_][\w.]*|@\".+\")?\s*(!)?\s*([a-zA-Z_][\w.]*|@\".+\")</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>keyword.operator.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#label</string>
</dict>
<dict>
<key>include</key>
<string>#param_list</string>
</dict>
<dict>
<key>match</key>
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
</array>
</dict>
<key>keywords</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\b(while|for|break|return|continue|asm|defer|errdefer|unreachable)\b</string>
<key>name</key>
<string>keyword.control.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(async|await|suspend|nosuspend|resume)\b</string>
<key>name</key>
<string>keyword.control.async.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(if|else|switch|try|catch|orelse)\b</string>
<key>name</key>
<string>keyword.control.conditional.zig</string>
</dict>
<dict>
<key>match</key>
<string>(?&lt;!\w)(@import|@cImport|@cInclude)\b</string>
<key>name</key>
<string>keyword.control.import.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(usingnamespace)\b</string>
<key>name</key>
<string>keyword.other.usingnamespace.zig</string>
</dict>
</array>
</dict>
<key>label</key>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>keyword.control.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.label.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>entity.name.label.zig</string>
</dict>
</dict>
<key>match</key>
<string>\b(break|continue)\s*:\s*([a-zA-Z_]\w*|@\".+\")\b|\b(?!\d)([a-zA-Z_]\w*|@\".+\")\b(?=\s*:\s*(?:\{|while\b))</string>
</dict>
<key>operators</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\b!\b</string>
<key>name</key>
<string>keyword.operator.zig</string>
</dict>
<dict>
<key>match</key>
<string>(==|(?:!|&gt;|&lt;)=?)</string>
<key>name</key>
<string>keyword.operator.logical.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(and|or)\b</string>
<key>name</key>
<string>keyword.operator.word.zig</string>
</dict>
<dict>
<key>match</key>
<string>((?:(?:\+|-|\*)\%?|/|%|&lt;&lt;|&gt;&gt;|&amp;|\|(?=[^\|])|\^)?=)</string>
<key>name</key>
<string>keyword.operator.assignment.zig</string>
</dict>
<dict>
<key>match</key>
<string>((?:\+|-|\*)\%?|/(?!/)|%)</string>
<key>name</key>
<string>keyword.operator.arithmetic.zig</string>
</dict>
<dict>
<key>match</key>
<string>(&lt;&lt;|&gt;&gt;|&amp;(?=[a-zA-Z_]|@\")|\|(?=[^\|])|\^|~)</string>
<key>name</key>
<string>keyword.operator.bitwise.zig</string>
</dict>
<dict>
<key>match</key>
<string>(\+\+|\*\*|-&gt;|\.\?|\.\*|&amp;(?=[a-zA-Z_]|@\")|\?|\|\||\.{2,3})</string>
<key>name</key>
<string>keyword.operator.other.zig</string>
</dict>
</array>
</dict>
<key>param_list</key>
<dict>
<key>begin</key>
<string>([a-zA-Z_]\w*|@\".+\")\s*(:)\s*</string>
<key>beginCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>variable.parameter.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.separator.zig</string>
</dict>
</dict>
<key>end</key>
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(?:(,)|(\)))</string>
<key>endCaptures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>punctuation.separator.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>punctuation.section.parens.end.zig</string>
</dict>
</dict>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#dummy_main</string>
</dict>
<dict>
<key>match</key>
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
<key>name</key>
<string>storage.type.zig</string>
</dict>
</array>
</dict>
<key>punctuation</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>,</string>
<key>name</key>
<string>punctuation.separator.zig</string>
</dict>
<dict>
<key>match</key>
<string>;</string>
<key>name</key>
<string>punctuation.terminator.zig</string>
</dict>
<dict>
<key>match</key>
<string>(\()</string>
<key>name</key>
<string>punctuation.section.parens.begin.zig</string>
</dict>
<dict>
<key>match</key>
<string>(\))</string>
<key>name</key>
<string>punctuation.section.parens.end.zig</string>
</dict>
</array>
</dict>
<key>storage</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>match</key>
<string>\b(bool|void|noreturn|type|anyerror|anytype)\b</string>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(?&lt;!\.)([iu]\d+|[iu]size|comptime_int)\b</string>
<key>name</key>
<string>storage.type.integer.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(f16|f32|f64|f128|comptime_float)\b</string>
<key>name</key>
<string>storage.type.float.zig</string>
</dict>
<dict>
<key>match</key>
<string>\b(c_short|c_ushort|c_int|c_uint|c_long|c_ulong|c_longlong|c_ulonglong|c_longdouble|c_void)\b</string>
<key>name</key>
<string>storage.type.c_compat.zig</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>keyword.operator.zig</string>
</dict>
<key>3</key>
<dict>
<key>name</key>
<string>storage.type.zig</string>
</dict>
</dict>
<key>match</key>
<string>\b(anyframe)\b\s*(-&gt;)?\s*(?:([a-zA-Z_][\w.]*|@\".+\")\b(?!\s*\())?</string>
</dict>
<dict>
<key>match</key>
<string>\bfn\b</string>
<key>name</key>
<string>storage.type.function.zig</string>
</dict>
<dict>
<key>match</key>
<string>\btest\b</string>
<key>name</key>
<string>storage.type.test.zig</string>
</dict>
<dict>
<key>match</key>
<string>\bstruct\b</string>
<key>name</key>
<string>storage.type.struct.zig</string>
</dict>
<dict>
<key>match</key>
<string>\benum\b</string>
<key>name</key>
<string>storage.type.enum.zig</string>
</dict>
<dict>
<key>match</key>
<string>\bunion\b</string>
<key>name</key>
<string>storage.type.union.zig</string>
</dict>
<dict>
<key>match</key>
<string>\berror\b</string>
<key>name</key>
<string>storage.type.error.zig</string>
</dict>
</array>
</dict>
<key>storage_modifier</key>
<dict>
<key>match</key>
<string>\b(const|var|extern|packed|export|pub|noalias|inline|noinline|comptime|volatile|align|linksection|threadlocal|allowzero)\b</string>
<key>name</key>
<string>storage.modifier.zig</string>
</dict>
<key>strings</key>
<dict>
<key>patterns</key>
<array>
<dict>
<key>begin</key>
<string>\'</string>
<key>end</key>
<string>\'</string>
<key>name</key>
<string>string.quoted.single.zig</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#character_escapes</string>
</dict>
<dict>
<key>match</key>
<string>\\[^\'][^\']*?</string>
<key>name</key>
<string>invalid.illegal.character.zig</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>c?\"</string>
<key>end</key>
<string>\"</string>
<key>name</key>
<string>string.quoted.double.zig</string>
<key>patterns</key>
<array>
<dict>
<key>include</key>
<string>#character_escapes</string>
</dict>
<dict>
<key>match</key>
<string>\\[^\'][^\']*?</string>
<key>name</key>
<string>invalid.illegal.character.zig</string>
</dict>
</array>
</dict>
<dict>
<key>begin</key>
<string>c?\\\\</string>
<key>end</key>
<string>$\n?</string>
<key>name</key>
<string>string.quoted.other.zig</string>
</dict>
</array>
</dict>
<key>support</key>
<dict>
<key>match</key>
<string>(?&lt;!\w)@[^\"\d][a-zA-Z_]\w*\b</string>
<key>name</key>
<string>support.function.zig</string>
</dict>
</dict>
<key>scopeName</key>
<string>source.zig</string>
<key>uuid</key>
<string>06C2FF99-3080-441A-9019-460C51E93116</string>
</dict>
</plist>

View file

@ -0,0 +1,204 @@
import { onceAsync } from "../lib.ts";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as oniguruma from "vscode-oniguruma";
import * as textmate from "vscode-textmate";
import { escapeHTML } from "../framework/bun-polyfill.ts";
const languages = [
"ts",
"tsx",
"zig",
"json",
"css",
"astro",
"mdx",
"lua",
"shell",
"dosbatch",
"powershell",
"yaml",
"toml",
"xml",
"python",
"php",
"diff",
] as const;
const altScopes: Record<string, string> = {
astro: "text.html.astro",
xml: "text.xml",
php: "text.html.php",
};
export type Language = (typeof languages)[number];
const scopes = [
// CSS
["punctuation.definition.keyword", "keyword", "css"],
["entity.name.tag.css", "class", "css"],
["meta.selector.css", "method", "css"],
["entity.other.attribute-name.class.css", "builtin", "css"],
["punctuation.definition.entity", "builtin", "css"],
["variable.css", "parameter", "css"],
// JSON
["support.type.property-name.json", "variable", "json"],
["constant.numeric", "method", "json"],
["constant", "class", "json"],
// Lua
["entity.name", "class", "lua"],
// Diff
["punctuation.definition.deleted", "variable", "diff"],
["markup.deleted", "variable", "diff"],
["punctuation.definition.inserted", "method", "diff"],
["markup.inserted", "method", "diff"],
["meta.diff.range", "string", "diff"],
["punctuation.definition.range", "string", "diff"],
["meta.toc-list-line.number", "keyword", "diff"],
["meta.diff", "comment", "diff"],
// General
["meta.object-literal.key", "property"],
["comment", "comment"],
["string", "string"],
["storage", "keyword"],
["keyword", "keyword"],
["variable.parameter", "parameter"],
["entity.name.function", "method"],
["support.type.primitive", "builtin"],
["entity.name.type", "class"],
["support.type", "class"],
["support.class", "class"],
["constant.language", "builtin"],
["constant", "constant"],
["support.constant", "constant"],
["meta.parameters", "parameter"],
["support.function", "method"],
["variable", "variable"],
["punctuation", null],
["meta.function-call", "method"],
] as const;
interface HighlightLinesOptions {
lines: string[];
grammar: textmate.IGrammar;
state: textmate.StateStack;
language: Language;
}
export function getStyle(scopesToCheck: string[], langugage: Language) {
if (import.meta.main) console.log(scopesToCheck);
for (const scope of scopes) {
if (scope[2] && scope[2] !== langugage) continue;
const find = scopesToCheck.find((s) => s.startsWith(scope[0]));
if (find) {
return scope[1];
}
}
return null;
}
function highlightLines({
lines,
grammar,
state,
language,
}: HighlightLinesOptions) {
let html = "";
let lastHtmlStyle: string | null = null;
const { length } = lines;
for (let i = 0; i < length; i += 1) {
const { tokens, ruleStack, stoppedEarly } = grammar.tokenizeLine(
lines[i],
state,
);
if (stoppedEarly) throw new Error("TODO: Tokenization stopped early?");
state = ruleStack;
for (const token of tokens) {
const str = lines[i].slice(token.startIndex, token.endIndex);
if (str.trim().length === 0) {
// Emit but do not consider scope changes
html += escapeHTML(str);
continue;
}
const style = getStyle(token.scopes, language);
if (style !== lastHtmlStyle) {
if (lastHtmlStyle) html += "</span>";
if (style) html += `<span class='${style}'>`;
}
html += escapeHTML(str);
lastHtmlStyle = style;
}
html += "\n";
}
if (lastHtmlStyle) html += "</span>";
return { state, html };
}
export const getRegistry = onceAsync(async () => {
const wasmBin = await fs.readFile(
path.join(
import.meta.dirname,
"../node_modules/vscode-oniguruma/release/onig.wasm",
),
);
await oniguruma.loadWASM(wasmBin);
return new textmate.Registry({
onigLib: Promise.resolve({
createOnigScanner: (patterns) => new oniguruma.OnigScanner(patterns),
createOnigString: (s) => new oniguruma.OnigString(s),
}),
loadGrammar: async (scopeName: string) => {
for (const lang of languages) {
if (scopeName.endsWith(`.${lang}`)) {
const file = await fs.readFile(
path.join(import.meta.dirname, `highlight-grammar/${lang}.plist`),
"utf-8",
);
return textmate.parseRawGrammar(file);
}
}
return null;
},
});
});
export async function highlightCode(code: string, language: Language) {
const registry = await getRegistry();
const grammar = await registry.loadGrammar(
altScopes[language] ?? "source." + language,
);
if (!grammar) {
throw new Error(`No grammar found for language: ${language}`);
}
let state = textmate.INITIAL;
const { html } = highlightLines({
lines: code.split("\n"),
grammar,
state,
language,
});
return html;
}
import { existsSync } from "node:fs";
if (import.meta.main) {
// validate exts
for (const ext of languages) {
if (
!existsSync(
path.join(import.meta.dirname, `highlight-grammar/${ext}.plist`),
)
) {
console.error(`Missing grammar for ${ext}`);
}
const html = await highlightCode("wwwwwwwwwwwaaaaaaaaaaaaaaaa", ext);
}
console.log(await highlightCode(`{"maps":"damn"`, "json"));
}

View file

View file

View file

View file

@ -0,0 +1,27 @@
import { MediaFile } from "../db";
import { useInlineScript } from "../framework/page-resources";
import { Readme } from "../media/cotyledon";
import { MediaPanel } from "../pages-dynamic/file_viewer";
import "../media/files.css";
export const theme = {
bg: "#312652",
fg: "#f0f0ff",
primary: "#fabe32",
};
export default function CotyledonPage() {
useInlineScript("canvas_cotyledon");
useInlineScript("file_viewer");
return (
<div class="files ctld ctld-et">
<MediaPanel
file={MediaFile.getByPath("/")!}
isLast={false}
activeFilename={null}
hasCotyledonCookie={true}
/>
<Readme />
</div>
);
}

View file

@ -0,0 +1,27 @@
import { MediaFile } from "../db";
import { useInlineScript } from "../framework/page-resources";
import { Speedbump } from "../media/cotyledon";
import { MediaPanel } from "../pages-dynamic/file_viewer";
import "../media/files.css";
export const theme = {
bg: "#312652",
fg: "#f0f0ff",
primary: "#fabe32",
};
export default function CotyledonPage() {
useInlineScript("canvas_cotyledon");
useInlineScript("file_viewer");
return (
<div class="files ctld ctld-sb">
<MediaPanel
file={MediaFile.getByPath("/")!}
isLast={false}
activeFilename={null}
hasCotyledonCookie={false}
/>
<Speedbump />
</div>
);
}

View file

@ -0,0 +1,9 @@
export const mediaRedirects: Record<string, string> = {
"/q+a/172533.png": "/q+a/172533.jpg",
"/q+a/2021-12-05_smooth.mp4": "/2019/smooth.mp4",
"/q+a/temp_2022-08-17-19-43-32.m4a":
"/2023/g-missing/fragments/2022-08-17-19-43-32.m4a",
"/q+a/2023-02-09_20-5814i.png":
"/2023/g-is-missing/fragments/2023-02-09_20-5814i.png",
"/2024/waterfalls/": "/2025/waterfalls/",
};

View file

@ -0,0 +1,233 @@
// Vibe coded with AI
(globalThis as any).canvas_2017 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
// Configuration interface for the checkerboard effect
interface CheckerboardConfig {
fps: number; // frames per second
color1: string; // first checkerboard color
color2: string; // second checkerboard color
opacity: number; // opacity of each checkerboard (0-1)
speedX1: number; // horizontal speed of first checkerboard (pixels per second)
speedY1: number; // vertical speed of first checkerboard (pixels per second)
speedX2: number; // horizontal speed of second checkerboard (pixels per second)
speedY2: number; // vertical speed of second checkerboard (pixels per second)
baseTileSize: number; // base size of checkerboard tiles
sizeVariation: number; // maximum variation in tile size (pixels)
sineFrequency1: number; // frequency of first sine wave for size variation
sineFrequency2: number; // frequency of second sine wave for size variation
sineOffset: number; // offset between the two sine waves (radians)
rotation: number; // rotation in degrees for the entire pattern
rotation2: number; // rotation in degrees for the entire pattern
}
// Default configuration
const config: CheckerboardConfig = {
fps: 30,
color1: "#1A1C17",
color2: "#1A1C17",
opacity: 0.3,
speedX1: -0.02, // moving left slowly
speedY1: -0.01, // moving up slowly
speedX2: -0.015, // moving left (slightly slower)
speedY2: 0.012, // moving down slowly
baseTileSize: 200,
sizeVariation: 1.5,
sineFrequency1: 0.0005,
sineFrequency2: 0.0008,
sineOffset: Math.PI / 2, // 90 degrees offset
rotation: 2, // 5 degree rotation
rotation2: -2, // 5 degree rotation
};
// Get the canvas context
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Could not get canvas context");
return () => {};
}
// Make canvas transparent
if (isStandalone) {
canvas.style.backgroundColor = "#737D60";
} else {
canvas.style.backgroundColor = "transparent";
}
// Variables to track position and animation
let width = canvas.width;
let height = canvas.height;
let animationFrameId: number;
let lastFrameTime = 0;
const frameInterval = 1000 / config.fps;
// Position offsets for the two checkerboards (centered)
let offset1X = 0;
let offset1Y = 0;
let offset2X = 0;
let offset2Y = 0;
// Time variable for sine wave calculation
let time = 0;
// Convert rotation to radians
const rotationRad = (config.rotation * Math.PI) / 180;
const rotationRad2 = (config.rotation2 * Math.PI) / 180;
// Update canvas dimensions when resized
const updateDimensions = () => {
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
};
// Calculate the diagonal length of the canvas (to ensure rotation covers corners)
const calculateDiagonal = () => {
return Math.sqrt(width * width + height * height);
};
// Draw a single checkerboard pattern scaled from center with rotation
const drawCheckerboard = (
offsetX: number,
offsetY: number,
tileSize: number,
color1: string,
color2: string,
opacity: number,
rotationRad: number,
) => {
ctx.globalAlpha = opacity;
// Get the center of the viewport
const centerX = width / 2;
const centerY = height / 2;
// Save the current transformation state
ctx.save();
// Move to the center of the canvas, rotate, then move back
ctx.translate(centerX, centerY);
ctx.rotate(rotationRad);
// Calculate the number of tiles needed to cover the rotated canvas
// We need to use the diagonal length to ensure we cover the corners when rotated
const diagonal = calculateDiagonal();
const tilesX = Math.ceil(diagonal / tileSize) + 6; // Added extra tiles for rotation
const tilesY = Math.ceil(diagonal / tileSize) + 6;
// Calculate how many tiles fit from center to edge (in each direction)
const halfTilesX = Math.ceil(tilesX / 2);
const halfTilesY = Math.ceil(tilesY / 2);
// Adjust the offset to be relative to the center
// The modulo ensures the pattern repeats smoothly even with scaling
const adjustedOffsetX = offsetX % (tileSize * 2);
const adjustedOffsetY = offsetY % (tileSize * 2);
// Draw the checker pattern, centered on the viewport
for (let y = -halfTilesY; y <= halfTilesY; y++) {
for (let x = -halfTilesX; x <= halfTilesX; x++) {
// Determine if this tile should be colored (creating checker pattern)
// We add a large number to ensure (x+y) is always positive for the modulo
if ((x + y + 1000) % 2 === 0) {
ctx.fillStyle = color1;
} else {
ctx.fillStyle = color2;
}
// Calculate the position of this tile relative to the center
// The adjusted offset creates the movement effect
const posX = (x * tileSize) + adjustedOffsetX;
const posY = (y * tileSize) + adjustedOffsetY;
// Draw the tile
ctx.fillRect(
posX - tileSize / 2,
posY - tileSize / 2,
tileSize,
tileSize,
);
}
}
// Restore the transformation state
ctx.restore();
};
// Animation loop
const animate = (currentTime: number) => {
animationFrameId = requestAnimationFrame(animate);
// Control frame rate
if (currentTime - lastFrameTime < frameInterval) {
return;
}
// Calculate the time elapsed since the last frame
const dt = currentTime - lastFrameTime;
lastFrameTime = currentTime;
// Increment time for sine wave calculation
time += dt;
// Update the position offsets based on speed and elapsed time
offset1X += config.speedX1 * dt;
offset1Y += config.speedY1 * dt;
offset2X += config.speedX2 * dt;
offset2Y += config.speedY2 * dt;
// Calculate the tile sizes using sine waves
const tileSize1 = config.baseTileSize +
Math.sin(time * config.sineFrequency1) * config.sizeVariation;
const tileSize2 = config.baseTileSize +
Math.sin(time * config.sineFrequency2 + config.sineOffset) *
config.sizeVariation;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw the two checkerboards
drawCheckerboard(
offset1X,
offset1Y,
tileSize1,
config.color1,
"transparent",
config.opacity,
rotationRad,
);
drawCheckerboard(
offset2X,
offset2Y,
tileSize2,
config.color2,
"transparent",
config.opacity,
rotationRad2,
);
// Reset global alpha
ctx.globalAlpha = 1.0;
};
// Initialize the animation
const init = () => {
// Set up resize handler
window.addEventListener("resize", updateDimensions);
// Initial setup
updateDimensions();
// Start animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
};
// Start the animation
init();
// Return cleanup function
return () => {
window.removeEventListener("resize", updateDimensions);
cancelAnimationFrame(animationFrameId);
};
};

View file

@ -0,0 +1,431 @@
// This canvas is based on the maze generation algo in Tanks. This was
// originally written in C++ as a single function in 2018, and was ported to TS
// by Chloe in 2025 for the cotyledon canvas.
//
// The main difference is that this version is a visualization, rather than the
// practical function. Instead of taking a millisecond, only 5 steps are
// performed per second, visualizing the whole ordeal. It also isn't a playable
// game, obviously.
//
// Ported with love because I care about my old self
// She deserves the world, but instead gave it to me.
(globalThis as any).canvas_2018 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
if (isStandalone) {
canvas.style.backgroundColor = "#27201E";
}
interface Cell {
down: boolean;
right: boolean;
visited: boolean;
cell_flash: number;
down_flash: number;
right_flash: number;
}
interface Pos {
x: number;
y: number;
/** Where the wall is relative to x, y. */
dir: "left" | "right" | "up" | "down";
}
interface Maze {
grid: Grid;
cursor: { x: number; y: number };
lastTick: number;
/* Pixels */
transform: number;
newCellsToVisit: Pos[];
randomWallBag: Cell[];
randomWallTarget: number;
renderOffset: { x: number; y: number };
done: boolean;
}
const hex = (color: number[]) =>
"#" + color.map((c) => c.toString(16).padStart(2, "0")).join("");
let cellSize: number;
let borderThickness: number;
const cellFlashModifier = isStandalone ? 0.4 : 0.2;
const color = isStandalone ? "#170d0b" : "#231C1A";
const bg = [0x27, 0x20, 0x1E];
const wallFlashColor = [0xFF, 0xA8, 0x7A];
const cellFlashColor = "#FFA87A";
const updateTime = 1000 / 7;
const randomWallBreakInterval = [6, 12]; // every 10 to 18 walls.
function randomBetween(min: number, max: number) {
return Math.round(
Math.random() * (max - min),
) + min;
}
function randomOf<T>(array: T[]): T {
return array[randomBetween(0, array.length - 1)];
}
function randomWallTarget() {
return randomBetween(
randomWallBreakInterval[0],
randomWallBreakInterval[1],
);
}
// Originally, this used a 2-dimensional array. However, I wanted to make sure
// that the grid could be infinitely sized. This grid constructs new cells on
// demand, as needed.
class Grid {
cells = new Map<number, Cell>();
cell({ x, y }: { x: number; y: number }) {
const k = ((x | 0) << 16) + (y | 0);
const { cells } = this;
let existing = this.cells.get(k);
if (!existing) {
existing = {
cell_flash: 0,
down: true,
down_flash: 0,
right: true,
right_flash: 0,
visited: false,
};
cells.set(k, existing);
}
return existing;
}
forAll(
renderOffset: { x: number; y: number },
width: number,
height: number,
cb: (cell: Cell, pos: { x: number; y: number }) => void,
) {
const { x: offsetX, y: offsetY } = renderOffset;
const startX = Math.floor(-offsetX / cellSize);
const startY = Math.floor(-offsetY / cellSize);
const endX = Math.ceil((width - offsetX) / cellSize);
const endY = Math.ceil((height - offsetY) / cellSize);
for (let x = startX; x <= endX; x++) {
for (let y = startY; y <= endY; y++) {
const cellX = offsetX + x * cellSize;
const cellY = offsetY + y * cellSize;
cb(this.cell({ x, y }), { x: cellX, y: cellY });
}
}
}
}
const ctx = canvas.getContext("2d")!;
if (!ctx) {
console.error("Could not get canvas context");
return () => {};
}
let width: number, height: number;
const updateDimensions = () => {
width = canvas.width = canvas.offsetWidth;
height = canvas.height = canvas.offsetHeight;
cellSize = 100;
borderThickness = 8;
};
updateDimensions();
setTimeout(() => {
updateDimensions();
}, 10);
let maze = initMaze();
let nextMaze: Maze | null = null;
let completeFade = 0;
function initMaze(): Maze {
return {
grid: new Grid(),
transform: 0,
cursor: {
x: randomBetween(0, Math.ceil(width / cellSize)),
y: randomBetween(0, Math.ceil(height / cellSize)),
},
lastTick: performance.now(),
randomWallBag: [],
randomWallTarget: randomWallTarget(),
newCellsToVisit: [],
renderOffset: { x: 0, y: 0 },
done: false,
};
}
function isOnScreen(maze: Maze, x: number, y: number) {
const { x: offsetX, y: offsetY } = maze.renderOffset;
const cellX = offsetX + x * cellSize;
const cellY = offsetY + y * cellSize;
return (
cellX + cellSize > 0 &&
cellX < width &&
cellY + cellSize > 0 &&
cellY < height
);
}
function tick(maze: Maze, other?: Maze) {
if (maze.done) return;
// The original maze algorithm broke down 4%-8% of random right facing
// walls, and 4%-8% of down facing walls. It did this at the end.
// To make this visual more interesting, two random walls will be broken
// down every 12-25 cell visits. This way, the main trail is always running.
if (maze.randomWallBag.length > maze.randomWallTarget) {
const down: Cell = randomOf(maze.randomWallBag);
const right: Cell = randomOf(maze.randomWallBag);
maze.randomWallBag.forEach((cell) =>
cell.cell_flash = Math.min(cell.cell_flash + 0.2, 1)
);
down.cell_flash = 1;
down.down = false;
down.down_flash = 1;
right.cell_flash = 1;
right.right = false;
right.right_flash = 1;
maze.randomWallBag = [];
maze.randomWallTarget = randomWallTarget();
return;
}
// The main algorithm was simple: Have a cursor position, and move it in a
// random direction that it had not seen before. Once it had run out of
// options, branch off of a previous location. Only visit each cell once.
//
// In this visualization, cells that are too far offscreen are softly
// treated as "visited", which is how the simulation always stays in frame.
const current = maze.grid.cell(maze.cursor);
current.visited = true;
current.cell_flash = 1;
maze.randomWallBag.push(current);
const adjacent = ([
{ x: maze.cursor.x + 1, y: maze.cursor.y, dir: "left" },
{ x: maze.cursor.x - 1, y: maze.cursor.y, dir: "right" },
{ x: maze.cursor.x, y: maze.cursor.y + 1, dir: "up" },
{ x: maze.cursor.x, y: maze.cursor.y - 1, dir: "down" },
] as Pos[]).filter((pos) =>
isOnScreen(maze, pos.x, pos.y) &&
maze.grid.cell(pos).visited === false
);
if (adjacent.length === 0) {
// move cursor to a random cell that has not been visited.
const cells = maze.newCellsToVisit.filter((pos) =>
isOnScreen(maze, pos.x, pos.y) &&
maze.grid.cell(pos).visited === false
);
if (cells.length === 0) {
maze.done = true;
return;
}
const continuePos = randomOf(cells);
breakWall(maze, continuePos, other);
maze.cursor = { x: continuePos.x, y: continuePos.y };
return;
}
// break a random wall
const toBreak = randomOf(adjacent);
breakWall(maze, toBreak, other);
maze.cursor = { x: toBreak.x, y: toBreak.y };
// add the other directions to the new cells to visit.
maze.newCellsToVisit.push(
...adjacent.filter((pos) => pos.dir !== toBreak.dir),
);
}
function breakWall(maze: Maze, pos: Pos, other?: Maze) {
if (pos.dir === "right") {
const cell = maze.grid.cell(pos);
cell.right = false;
cell.right_flash = 1;
if (other) cell.right = false;
} else if (pos.dir === "down") {
const cell = maze.grid.cell(pos);
cell.down = false;
cell.down_flash = 1;
if (other) cell.down = false;
} else if (pos.dir === "left") {
const cell = maze.grid.cell({ x: pos.x - 1, y: pos.y });
cell.right = false;
cell.right_flash = 1;
if (other) cell.right = false;
} else if (pos.dir === "up") {
const cell = maze.grid.cell({ x: pos.x, y: pos.y - 1 });
cell.down = false;
cell.down_flash = 1;
if (other) cell.down = false;
}
}
function renderOffset(maze: Maze) {
return { x: maze.transform, y: maze.transform };
}
let animationFrameId: number;
let last = performance.now();
let dt: number = 0;
function renderMazeBorders(maze: Maze, opacity: number) {
ctx.globalAlpha = opacity;
maze.grid.forAll(
maze.renderOffset,
width,
height,
(cell, { x: cellX, y: cellY }) => {
// Walls
if (cell.right) {
ctx.fillStyle = color;
ctx.fillRect(
cellX + cellSize - borderThickness / 2,
cellY - borderThickness / 2,
borderThickness,
cellSize + borderThickness,
);
}
if (cell.down) {
ctx.fillStyle = color;
ctx.fillRect(
cellX - borderThickness / 2,
cellY + cellSize - borderThickness / 2,
cellSize + borderThickness,
borderThickness,
);
}
},
);
ctx.globalAlpha = 1;
}
function renderCellFlash(maze: Maze) {
maze.grid.forAll(
maze.renderOffset,
width,
height,
(cell, { x: cellX, y: cellY }) => {
// Cell flash to show visiting path.
if (cell.cell_flash > 0) {
cell.cell_flash = Math.max(0, cell.cell_flash - dt / 1000);
ctx.fillStyle = cellFlashColor;
ctx.globalAlpha = cell.cell_flash * cellFlashModifier;
ctx.fillRect(cellX, cellY, cellSize, cellSize);
ctx.globalAlpha = 1;
}
},
);
}
function renderBorderFlash(maze: Maze) {
maze.grid.forAll(
maze.renderOffset,
width,
height,
(cell, { x: cellX, y: cellY }) => {
if (cell.right_flash == 0 && cell.down_flash == 0) {
return;
}
// Walls
const cellFlash = cell.cell_flash * cellFlashModifier;
if (cell.right_flash > 0) {
cell.right_flash = Math.max(0, cell.right_flash - dt / 500);
ctx.fillStyle = interpolateColor(
bg,
wallFlashColor,
Math.max(cell.right_flash, cellFlash),
);
if (cellFlash > cell.right_flash) {
ctx.globalAlpha = cell.right_flash / cellFlash;
}
ctx.fillRect(
cellX + cellSize - borderThickness / 2,
cellY + borderThickness / 2,
borderThickness,
cellSize - borderThickness,
);
ctx.globalAlpha = 1;
}
if (cell.down_flash > 0) {
if (cellFlash > cell.down_flash) {
ctx.globalAlpha = cell.down_flash / cellFlash;
}
cell.down_flash = Math.max(0, cell.down_flash - dt / 500);
ctx.fillStyle = interpolateColor(
bg,
wallFlashColor,
Math.max(cell.down_flash, cellFlash),
);
ctx.fillRect(
cellX + borderThickness / 2,
cellY + cellSize - borderThickness / 2,
cellSize - borderThickness,
borderThickness,
);
ctx.globalAlpha = 1;
}
},
);
}
function render() {
const now = performance.now();
dt = now - last;
maze.transform += dt * 0.005;
maze.renderOffset = renderOffset(maze);
if (!maze.done) {
if (now - maze.lastTick >= updateTime) {
tick(maze);
maze.lastTick = now;
if (maze.done) {
nextMaze = initMaze();
nextMaze.transform = (maze.transform % cellSize) - dt * 0.005;
nextMaze.lastTick = now;
completeFade = 0;
}
}
}
if (nextMaze) {
nextMaze.transform += dt * 0.005;
nextMaze.renderOffset = renderOffset(nextMaze);
if (!nextMaze.done && now - nextMaze.lastTick >= updateTime) {
tick(nextMaze, maze);
nextMaze.lastTick = now;
}
}
last = now;
ctx.clearRect(0, 0, width, height);
renderCellFlash(maze);
if (nextMaze) renderCellFlash(nextMaze);
renderMazeBorders(maze, 1);
if (nextMaze) {
renderMazeBorders(nextMaze, completeFade);
completeFade += dt / 3000;
if (completeFade >= 1) {
maze = nextMaze;
nextMaze = null;
}
}
renderBorderFlash(maze);
if (nextMaze) {
renderCellFlash(nextMaze);
renderBorderFlash(nextMaze);
}
animationFrameId = requestAnimationFrame(render);
}
function interpolateColor(start: number[], end: number[], t: number) {
return hex(start.map((s, i) => Math.round(s + (end[i] - s) * t)));
}
window.addEventListener("resize", updateDimensions);
animationFrameId = requestAnimationFrame(render);
// cleanup function
return () => {
window.removeEventListener("resize", updateDimensions);
cancelAnimationFrame(animationFrameId);
};
};

View file

@ -0,0 +1,213 @@
// Ported from CanvasAPI, allegedly written on 2019-08-26.
(globalThis as any).canvas_2019 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
if (isStandalone) {
canvas.parentElement!.style.backgroundColor = "#121013";
}
// Canvas.tsx
abstract class CanvasAPI {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
width = 0;
height = 0;
private _disposed = false;
private _running = false;
private _last = 0;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.width = canvas.width = canvas.clientWidth;
this.height = canvas.height = canvas.clientHeight;
const ctx = this.canvas.getContext("2d");
if (!ctx) {
throw new Error("Canvas2D Not Supported!");
}
this.ctx = ctx;
}
stopRenderLoop() {
this._running = false;
}
startRenderLoop() {
if (this._disposed) return;
this._running = true;
this._last = performance.now();
requestAnimationFrame(this._renderLoop);
}
private _renderLoop = (delta: number) => {
if (!this._running) return;
this.render(delta - this._last);
this._last = delta;
requestAnimationFrame(this._renderLoop);
};
abstract render(delta: number): void;
}
// VaultBackground.ts
function addGSHelper(
grad: CanvasGradient,
color: string,
dotOpacity: number,
gradStop: number,
gradOpacity: number,
) {
grad.addColorStop(gradStop, `rgba(${color},${dotOpacity * gradOpacity})`);
}
function randAround(target: number, dist: number) {
return (Math.random() - 0.5) * dist * 2 + target;
}
class Dot {
x = Math.random() * 1.1 - 0.05;
y = Math.random() / 4 + 0.9;
size = Math.random() * 200 + 50;
opacity = 0;
opacityRandom = Math.random() / 3 + 0.3;
fadeInOpacity = 1;
color = `${randAround(217, 30)}, ${randAround(170, 30)}, ${
randAround(255, 20)
}`;
life = 0;
ySpeed_1 = 0;
ySpeed_2 = -0.0000063;
ySpeed_3 = 0.000000016;
ySpeed_4 = 0.000000000009;
seed = Math.random();
delete = false;
update(init: boolean) {
this.life += 0.8;
if (this.life < 115) {
this.opacity = this.life / 230;
} else if (this.life > 450) {
this.delete = true;
} else if (this.life > 300) {
this.opacity = (150 + 300 - this.life) / 300;
}
this.ySpeed_3 += this.ySpeed_4;
this.ySpeed_2 += this.ySpeed_3;
this.ySpeed_1 += this.ySpeed_2;
this.y += this.ySpeed_1 * 0.5;
this.size -= 0.08;
if (this.delete) {
Object.assign(this, new Dot());
}
}
render(scene: VaultBackground) {
const ctx = scene.ctx;
if (this.fadeInOpacity < 1) {
this.fadeInOpacity += 0.0075;
}
const finalX = this.x +
Math.sin(this.seed * Math.PI * 2 + Date.now() / 15000) * 0.2;
const drawX = scene.shakeX +
finalX * Math.max(700, scene.width) -
(Math.max(700, scene.width) - scene.width) / 2;
const drawY = scene.shakeY + (this.y * 1.5 - 0.5) * scene.height;
const opacity = this.opacity * this.opacityRandom * this.fadeInOpacity;
const grad = ctx.createRadialGradient(
drawX,
drawY,
0,
drawX,
drawY,
this.size,
);
addGSHelper(grad, this.color, opacity, 0, 1);
addGSHelper(grad, this.color, opacity, 0.8, 0.7);
addGSHelper(grad, this.color, opacity, 0.87, 0.5);
addGSHelper(grad, this.color, opacity, 0.93, 0.3);
addGSHelper(grad, this.color, opacity, 1, 0);
ctx.fillStyle = grad;
ctx.fillRect(
drawX - this.size,
drawY - this.size,
this.size * 2,
this.size * 2,
);
}
}
class VaultBackground extends CanvasAPI {
private items = new Set<Dot>();
private shakeVar = 0;
private dom?: HTMLElement;
shakeX = 0;
shakeY = 0;
constructor(canvas: HTMLCanvasElement) {
super(canvas);
for (let i = 0; i < 450; i++) {
if (i % 7 === 0) {
this.items.add(new Dot());
}
this.items.forEach((x) => x.update(true));
}
this.items.forEach((x) => x.fadeInOpacity = 0);
}
render(): void {
this.ctx.clearRect(0, 0, this.width, this.height);
this.items.forEach((x) => (x.update(false), x.render(this)));
if (this.shakeVar >= 0.0001) {
this.shakeVar *= 0.97 - 0.22 * this.shakeVar;
if (this.shakeVar >= 0.0001) {
this.shakeX = (Math.random() * 2 - 1) * this.shakeVar * 65;
this.shakeY = (Math.random() * 2 - 1) * this.shakeVar * 65;
if (this.dom) {
this.dom.style.transform =
`translate(${this.shakeX}px,${this.shakeY}px)`;
}
} else {
this.shakeX = 0;
this.shakeY = 0;
if (this.dom) this.dom.style.removeProperty("transform");
this.dom = undefined;
}
}
}
shake(dom?: HTMLElement | null) {
this.dom = dom || document.body;
this.shakeVar = 1;
}
}
// Binding code
let bg = new VaultBackground(canvas);
bg.startRenderLoop();
canvas.style.opacity = "0.2";
function onResize() {
bg.width = canvas.width = canvas.clientWidth;
bg.height = canvas.height = canvas.clientHeight;
}
window.addEventListener("resize", onResize);
onResize();
(globalThis as any).vault = bg;
return () => {
bg.stopRenderLoop();
window.removeEventListener("resize", onResize);
};
};

View file

@ -0,0 +1,197 @@
// Vibe coded with AI
(globalThis as any).canvas_2020 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
// Rain effect with slanted lines
// Configuration interface for the rain effect
interface RainConfig {
fps: number; // frames per second
color: string; // color of rain particles
angle: number; // angle in degrees
particleDensity: number; // particles per 10000 pixels of canvas area
speed: number; // speed of particles (pixels per frame)
lineWidth: number; // thickness of rain lines
lineLength: number; // length of rain lines
}
// Rain particle interface
interface RainParticle {
x: number; // x position
y: number; // y position
}
// Default configuration
const config: RainConfig = {
fps: 16,
color: isStandalone ? "#00FEFB99" : "#081F24",
angle: -18,
particleDensity: 1,
speed: 400,
lineWidth: 8,
lineLength: 100,
};
// Get the canvas context
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Could not get canvas context");
return () => {};
}
// Make canvas transparent
if (isStandalone) {
canvas.style.backgroundColor = "#0F252B";
} else {
canvas.style.backgroundColor = "transparent";
}
// Calculate canvas dimensions and update when resized
let width = canvas.width;
let height = canvas.height;
let particles: RainParticle[] = [];
let animationFrameId: number;
let lastFrameTime = 0;
const frameInterval = 1000 / config.fps;
// Calculate angle in radians
const angleRad = (config.angle * Math.PI) / 180;
// Update canvas dimensions and particle count when resized
const updateDimensions = () => {
width = canvas.width = canvas.offsetWidth;
height = canvas.height = canvas.offsetHeight;
// Calculate the canvas area in pixels
const canvasArea = width * height;
// Calculate target number of particles based on canvas area
const targetParticleCount = Math.floor(
(canvasArea / 10000) * config.particleDensity,
);
// Calculate buffer for horizontal offset due to slanted angle
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
// Adjust the particles array
if (particles.length < targetParticleCount) {
// Add more particles if needed
for (let i = particles.length; i < targetParticleCount; i++) {
particles.push(createParticle(true, buffer));
}
} else if (particles.length > targetParticleCount) {
// Remove excess particles
particles = particles.slice(0, targetParticleCount);
}
};
// Create a new particle
// Added initialDistribution parameter to distribute particles across the entire canvas at startup
const createParticle = (
initialDistribution = false,
buffer: number,
): RainParticle => {
// For initial distribution, place particles throughout the canvas
// Otherwise start them above the canvas
let x = Math.random() * (width + buffer * 2) - buffer;
let y;
if (initialDistribution) {
// Distribute across the entire canvas height for initial setup
y = Math.random() * (height + config.lineLength * 2) - config.lineLength;
} else {
// Start new particles from above the canvas with some randomization
y = -config.lineLength - (Math.random() * config.lineLength * 20);
}
return {
x,
y,
};
};
// Update particle positions
const updateParticles = () => {
// Calculate buffer for horizontal offset due to slanted angle
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
// Update position based on speed and angle
p.x += Math.sin(angleRad) * config.speed;
p.y += Math.cos(angleRad) * config.speed;
// Reset particles that go offscreen - only determined by position
// Add extra buffer to ensure particles fully exit the visible area before resetting
if (
p.y > height + config.lineLength ||
p.x < -buffer ||
p.x > width + buffer
) {
particles[i] = createParticle(false, buffer);
}
}
};
// Draw particles
const drawParticles = () => {
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Set drawing properties
ctx.strokeStyle = config.color;
ctx.lineWidth = config.lineWidth;
ctx.lineCap = "square";
// Draw each rain line
ctx.beginPath();
for (const p of particles) {
// Only draw particles that are either on screen or within a reasonable buffer
// This is for performance reasons - we don't need to draw particles far offscreen
if (p.y >= -config.lineLength * 2 && p.y <= height + config.lineLength) {
const endX = p.x + Math.sin(angleRad) * config.lineLength;
const endY = p.y + Math.cos(angleRad) * config.lineLength;
ctx.moveTo(p.x, p.y);
ctx.lineTo(endX, endY);
}
}
ctx.stroke();
};
// Animation loop
const animate = (currentTime: number) => {
animationFrameId = requestAnimationFrame(animate);
// Control frame rate
if (currentTime - lastFrameTime < frameInterval) {
return;
}
lastFrameTime = currentTime;
updateParticles();
drawParticles();
};
// Initialize the animation
const init = () => {
// Set up resize handler
window.addEventListener("resize", updateDimensions);
// Initial setup
updateDimensions();
// Start animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
};
// Start the animation
init();
// Return cleanup function
return () => {
window.removeEventListener("resize", updateDimensions);
cancelAnimationFrame(animationFrameId);
};
};

View file

@ -0,0 +1,783 @@
// Vibe coded.
(globalThis as any).canvas_2021 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
// Constants for simulation
const PARTICLE_RADIUS = 4.5;
const PARTICLE_DENSITY = 0.004; // Particles per pixel
const MIN_SPEED = 0.05;
const MAX_SPEED = 6.0;
const FRICTION = 0.96;
const REPULSION_STRENGTH = 0.1;
const REPULSION_RADIUS = 50;
const FORCE_RADIUS = 400; // Increased radius
const FORCE_STRENGTH = 0.25;
const FORCE_FALLOFF_EXPONENT = 3; // Higher value = sharper falloff
const FORCE_SPACING = 10; // Pixels between force points
const MIN_FORCE_STRENGTH = 0.05; // Minimum force strength for very slow movements
const MAX_FORCE_STRENGTH = 0.4; // Maximum force strength for fast movements
const MIN_SPEED_THRESHOLD = 1; // Movement speed (px/frame) that produces minimum force
const MAX_SPEED_THRESHOLD = 20; // Movement speed that produces maximum force
const OVERSCAN_PIXELS = 250;
const CELL_SIZE = REPULSION_RADIUS; // For spatial hashing
let globalOpacity = 0;
if (isStandalone) {
canvas.style.backgroundColor = "#301D02";
} else {
canvas.style.backgroundColor = "transparent";
}
// Interfaces
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
charge: number; // 0 to 1, affecting color
}
interface Force {
x: number;
y: number;
dx: number;
dy: number;
strength: number;
radius: number;
createdAt: number;
}
interface SpatialHash {
[key: string]: Particle[];
}
// State
let first = true;
let particles: Particle[] = [];
let forces: Force[] = [];
let width = canvas.width;
let height = canvas.height;
let targetParticleCount = 0;
let spatialHash: SpatialHash = {};
let ctx: CanvasRenderingContext2D | null = null;
let animationId: number | null = null;
let isRunning = false;
// Mouse tracking
let lastMousePosition: { x: number; y: number } | null = null;
// Track position of the last created force
let lastForcePosition: { x: number; y: number } | null = null;
// Keep track of previous canvas dimensions for resize logic
let previousWidth = 0;
let previousHeight = 0;
// Initialize and cleanup
function init(): void {
ctx = canvas.getContext("2d");
if (!ctx) return;
// Set canvas to full size
resizeCanvas();
// Event listeners
window.addEventListener("resize", resizeCanvas);
document.addEventListener("mousemove", handleMouseMove);
// Start animation immediately
start();
}
function cleanup(): void {
// Stop the animation
stop();
// Remove event listeners
window.removeEventListener("resize", resizeCanvas);
document.removeEventListener("mousemove", handleMouseMove);
// Clear arrays
particles = [];
forces = [];
spatialHash = {};
lastMousePosition = null;
lastForcePosition = null;
}
// Resize canvas and adjust particle count
function resizeCanvas(): void {
// Store previous dimensions
previousWidth = width;
previousHeight = height;
// Update to new dimensions
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const oldTargetCount = targetParticleCount;
targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY);
// Adjust particle count
if (targetParticleCount > oldTargetCount) {
// Add more particles if needed, but only in newly available space
addParticles(targetParticleCount - oldTargetCount, !first);
first = false;
}
// Note: Removal of excess particles happens naturally during update
}
// Handle mouse movement
function handleMouseMove(e: MouseEvent): void {
const rect = canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
// Initialize positions if this is the first movement
if (!lastMousePosition || !lastForcePosition) {
lastMousePosition = { x: currentX, y: currentY };
lastForcePosition = { x: currentX, y: currentY };
return;
}
// Store current mouse position
const mouseX = currentX;
const mouseY = currentY;
// Calculate vector from last mouse position to current
const dx = mouseX - lastMousePosition.x;
const dy = mouseY - lastMousePosition.y;
const distMoved = Math.sqrt(dx * dx + dy * dy);
// Skip if essentially no movement (avoids numerical issues)
if (distMoved < 0.1) {
return;
}
// Get the vector from the last force to the current mouse position
const forceDx = mouseX - lastForcePosition.x;
const forceDy = mouseY - lastForcePosition.y;
const forceDistance = Math.sqrt(forceDx * forceDx + forceDy * forceDy);
// Only create forces if we've moved far enough from the last force
if (forceDistance >= FORCE_SPACING) {
// Calculate the direction vector from last force to current mouse
let dirX = forceDx / forceDistance;
let dirY = forceDy / forceDistance;
// Calculate how many force points to create
const numPoints = Math.floor(forceDistance / FORCE_SPACING);
// Calculate movement speed based on the recent movement
const movementSpeed = distMoved; // Simple approximation of speed
// Scale force strength based on movement speed
let speedFactor;
if (movementSpeed <= MIN_SPEED_THRESHOLD) {
speedFactor = MIN_FORCE_STRENGTH;
} else if (movementSpeed >= MAX_SPEED_THRESHOLD) {
speedFactor = MAX_FORCE_STRENGTH;
} else {
// Linear interpolation between min and max
const t = (movementSpeed - MIN_SPEED_THRESHOLD) /
(MAX_SPEED_THRESHOLD - MIN_SPEED_THRESHOLD);
speedFactor = MIN_FORCE_STRENGTH +
t * (MAX_FORCE_STRENGTH - MIN_FORCE_STRENGTH);
}
// Store current force position to update incrementally
let currentForceX = lastForcePosition.x;
let currentForceY = lastForcePosition.y;
// Create evenly spaced force points along the path from last force to current mouse
for (let i = 0; i < numPoints; i++) {
// Calculate position for this force point
const t = (i + 1) / numPoints;
const fx = lastForcePosition.x + forceDx * t;
const fy = lastForcePosition.y + forceDy * t;
// Create force at this position with the direction vector
createForce(fx, fy, dirX, dirY, speedFactor);
// Update the last force position to this new force
currentForceX = fx;
currentForceY = fy;
}
// Update the last force position
lastForcePosition = { x: currentForceX, y: currentForceY };
}
// Always update the last mouse position
lastMousePosition = { x: mouseX, y: mouseY };
}
// Create a new force
function createForce(
x: number,
y: number,
dx: number,
dy: number,
strength = FORCE_STRENGTH,
): void {
forces.push({
x,
y,
dx,
dy,
strength,
radius: 1,
createdAt: Date.now(),
});
}
// Improved particle addition with fill strategy options
function addParticles(count: number, inNewAreaOnly: boolean = false): void {
// Determine available space
const minX = -OVERSCAN_PIXELS;
const maxX = width + OVERSCAN_PIXELS;
const minY = -OVERSCAN_PIXELS;
const maxY = height + OVERSCAN_PIXELS;
// Use a grid system that guarantees uniform spacing of particles
const gridSpacing = REPULSION_RADIUS * 0.8; // Slightly less than repulsion radius
const gridWidth = Math.ceil((maxX - minX) / gridSpacing);
const gridHeight = Math.ceil((maxY - minY) / gridSpacing);
// Track which grid cells are already occupied
const occupiedCells: Set<string> = new Set();
// Mark cells occupied by existing particles
for (const particle of particles) {
const cellX = Math.floor((particle.x - minX) / gridSpacing);
const cellY = Math.floor((particle.y - minY) / gridSpacing);
// Ensure cell coordinates are within valid range
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
occupiedCells.add(`${cellX},${cellY}`);
}
}
// Create arrays of all cells and filter by placement strategy
const allGridCells: { x: number; y: number }[] = [];
for (let cellY = 0; cellY < gridHeight; cellY++) {
for (let cellX = 0; cellX < gridWidth; cellX++) {
const cellKey = `${cellX},${cellY}`;
if (!occupiedCells.has(cellKey)) {
const posX = minX + (cellX + 0.5) * gridSpacing;
const posY = minY + (cellY + 0.5) * gridSpacing;
// For new area only placement, filter to expanded areas
if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) {
const expandedRight = width > previousWidth;
const expandedBottom = height > previousHeight;
const inNewRightArea = expandedRight && posX >= previousWidth &&
posX <= width;
const inNewBottomArea = expandedBottom && posY >= previousHeight &&
posY <= height;
if (inNewRightArea || inNewBottomArea) {
allGridCells.push({ x: cellX, y: cellY });
}
} else if (!inNewAreaOnly) {
// Standard placement - add all valid cells
allGridCells.push({ x: cellX, y: cellY });
}
}
}
}
if (allGridCells.length == 0) {
throw new Error("No cells available to place particles");
}
// We now have all grid cells that match our placement criteria
// If we need more particles than we have available cells, we need to adjust
// gridSpacing to fit more cells into the same space
if (count > allGridCells.length) {
// Retry with a smaller grid spacing
// Proportionally reduce the grid spacing to fit the required number of particles
const scaleFactor = Math.sqrt(allGridCells.length / count);
const newGridSpacing = gridSpacing * scaleFactor;
// Clear particles and try again with new spacing
// This is a recursive call, but with adjusted parameters that will fit
return addParticlesWithCustomSpacing(
count,
inNewAreaOnly,
newGridSpacing,
);
}
// Shuffle the available cells for random selection
shuffleArray(allGridCells);
// Take the number of cells we need
const cellsToUse = Math.min(count, allGridCells.length);
const selectedCells = allGridCells.slice(0, cellsToUse);
// Create particles in selected cells
for (const cell of selectedCells) {
// Add jitter within the cell for natural look
const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8;
const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8;
// Calculate final position
const x = minX + (cell.x + 0.5) * gridSpacing + jitterX;
const y = minY + (cell.y + 0.5) * gridSpacing + jitterY;
// Create a particle at this position
particles.push(createParticle(x, y));
}
}
// Helper function to add particles with custom grid spacing
function addParticlesWithCustomSpacing(
count: number,
inNewAreaOnly: boolean,
gridSpacing: number,
): void {
if (gridSpacing == 0) throw new Error("Grid spacing is 0");
// Determine available space
const minX = -OVERSCAN_PIXELS;
const maxX = width + OVERSCAN_PIXELS;
const minY = -OVERSCAN_PIXELS;
const maxY = height + OVERSCAN_PIXELS;
// Create grid using the custom spacing
const gridWidth = Math.ceil((maxX - minX) / gridSpacing);
const gridHeight = Math.ceil((maxY - minY) / gridSpacing);
// Track which grid cells are already occupied
const occupiedCells: Set<string> = new Set();
// Mark cells occupied by existing particles
for (const particle of particles) {
const cellX = Math.floor((particle.x - minX) / gridSpacing);
const cellY = Math.floor((particle.y - minY) / gridSpacing);
// Ensure cell coordinates are within valid range
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
occupiedCells.add(`${cellX},${cellY}`);
}
}
// Create arrays of all cells and filter by placement strategy
const allGridCells: { x: number; y: number }[] = [];
for (let cellY = 0; cellY < gridHeight; cellY++) {
for (let cellX = 0; cellX < gridWidth; cellX++) {
const cellKey = `${cellX},${cellY}`;
if (!occupiedCells.has(cellKey)) {
const posX = minX + (cellX + 0.5) * gridSpacing;
const posY = minY + (cellY + 0.5) * gridSpacing;
// For new area only placement, filter to expanded areas
if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) {
const expandedRight = width > previousWidth;
const expandedBottom = height > previousHeight;
const inNewRightArea = expandedRight && posX >= previousWidth &&
posX <= width;
const inNewBottomArea = expandedBottom && posY >= previousHeight &&
posY <= height;
if (inNewRightArea || inNewBottomArea) {
allGridCells.push({ x: cellX, y: cellY });
}
} else if (!inNewAreaOnly) {
// Standard placement - add all valid cells
allGridCells.push({ x: cellX, y: cellY });
}
}
}
}
// Shuffle the available cells for random distribution
shuffleArray(allGridCells);
// Take the number of cells we need (or all if we have fewer)
const cellsToUse = Math.min(count, allGridCells.length);
// Create particles in selected cells
for (let i = 0; i < cellsToUse; i++) {
const cell = allGridCells[i];
// Add jitter within the cell
const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8;
const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8;
// Calculate final position
const x = minX + (cell.x + 0.5) * gridSpacing + jitterX;
const y = minY + (cell.y + 0.5) * gridSpacing + jitterY;
// Create a particle at this position
particles.push(createParticle(x, y));
}
}
// Utility to shuffle an array (Fisher-Yates algorithm)
function shuffleArray<T>(array: T[]): void {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// Simplified createParticle function that just places at a specific position
function createParticle(x: number, y: number): Particle {
return {
x: x + (Math.random() * 4 - 2),
y: y + (Math.random() * 4 - 2),
vx: 0,
vy: 0,
charge: 0,
};
}
// Function to create a particle on one of the edges
function createParticleOnEdge(): Particle {
// Overscan bounds with fixed pixel size
const minX = -OVERSCAN_PIXELS;
const maxX = width + OVERSCAN_PIXELS;
const minY = -OVERSCAN_PIXELS;
const maxY = height + OVERSCAN_PIXELS;
let x: number, y: number;
// Place on one of the edges
const edge = Math.floor(Math.random() * 4);
switch (edge) {
case 0: // Top
x = minX + Math.random() * (maxX - minX);
y = minY;
break;
case 1: // Right
x = maxX;
y = minY + Math.random() * (maxY - minY);
break;
case 2: // Bottom
x = minX + Math.random() * (maxX - minX);
y = maxY;
break;
case 3: // Left
x = minX;
y = minY + Math.random() * (maxY - minY);
break;
default:
x = minX + Math.random() * (maxX - minX);
y = minY + Math.random() * (maxY - minY);
}
return createParticle(x, y);
}
// Spatial hashing functions
function getHashKey(x: number, y: number): string {
const cellX = Math.floor(x / CELL_SIZE);
const cellY = Math.floor(y / CELL_SIZE);
return `${cellX},${cellY}`;
}
function addToSpatialHash(particle: Particle): void {
const key = getHashKey(particle.x, particle.y);
if (!spatialHash[key]) {
spatialHash[key] = [];
}
spatialHash[key].push(particle);
}
function updateSpatialHash(): void {
// Clear previous hash
spatialHash = {};
// Add all particles to hash
for (const particle of particles) {
addToSpatialHash(particle);
}
}
function getNearbyParticles(
x: number,
y: number,
radius: number,
): Particle[] {
const result: Particle[] = [];
const cellRadius = Math.ceil(radius / CELL_SIZE);
const centerCellX = Math.floor(x / CELL_SIZE);
const centerCellY = Math.floor(y / CELL_SIZE);
for (
let cellX = centerCellX - cellRadius;
cellX <= centerCellX + cellRadius;
cellX++
) {
for (
let cellY = centerCellY - cellRadius;
cellY <= centerCellY + cellRadius;
cellY++
) {
const key = `${cellX},${cellY}`;
const cell = spatialHash[key];
if (cell) {
result.push(...cell);
}
}
}
return result;
}
// Main update function
function update(): void {
const now = Date.now();
// Fixed pixel overscan
const minX = -OVERSCAN_PIXELS;
const maxX = width + OVERSCAN_PIXELS;
const minY = -OVERSCAN_PIXELS;
const maxY = height + OVERSCAN_PIXELS;
// Update spatial hash
updateSpatialHash();
// Update forces and remove expired ones
if (forces.length > 40) {
forces = forces.slice(-40);
}
forces = forces.filter((force) => {
force.strength *= 0.95;
force.radius *= 0.95;
return force.strength > 0.001;
});
// Update particles
const newParticles: Particle[] = [];
for (const particle of particles) {
// Apply forces
for (const force of forces) {
const dx = particle.x - force.x;
const dy = particle.y - force.y;
const distSq = dx * dx + dy * dy;
const radius = force.radius * FORCE_RADIUS;
if (distSq < radius * radius) {
const dist = Math.sqrt(distSq);
// Exponential falloff - much more concentrated at center
// (1 - x/R)^n where n controls how sharp the falloff is
const normalizedDist = dist / radius;
const factor = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT);
// Calculate force line projection for directional effect
// This makes particles along the force's path experience stronger effect
const dotProduct = (dx * -force.dx) + (dy * -force.dy);
const projectionFactor = Math.max(0, dotProduct / dist);
// Apply the combined factors - stronger directional bias
const finalFactor = factor * force.strength *
(0.1 + 0.9 * projectionFactor);
particle.vx += force.dx * finalFactor;
particle.vy += force.dy * finalFactor;
// charge for the first 100ms
if ((now - force.createdAt) < 100) {
particle.charge = Math.min(
1,
particle.charge + (finalFactor * finalFactor) * 0.2,
);
}
}
}
// Apply repulsion from nearby particles
const nearby = getNearbyParticles(
particle.x,
particle.y,
REPULSION_RADIUS,
);
for (const other of nearby) {
if (other === particle) continue;
const dx = particle.x - other.x;
const dy = particle.y - other.y;
const distSq = dx * dx + dy * dy;
if (distSq < REPULSION_RADIUS * REPULSION_RADIUS && distSq > 0) {
const dist = Math.sqrt(distSq);
const factor = REPULSION_STRENGTH * (1 - dist / REPULSION_RADIUS);
const fx = dx / dist * factor;
const fy = dy / dist * factor;
particle.vx += fx;
particle.vy += fy;
}
}
// Apply friction
particle.vx *= FRICTION;
particle.vy *= FRICTION;
// Ensure minimum speed
const speed = Math.sqrt(
particle.vx * particle.vx + particle.vy * particle.vy,
);
if (speed < MIN_SPEED && speed > 0) {
const scale = MIN_SPEED / speed;
particle.vx *= scale;
particle.vy *= scale;
}
// Cap at maximum speed
if (speed > MAX_SPEED) {
const scale = MAX_SPEED / speed;
particle.vx *= scale;
particle.vy *= scale;
}
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Decrease charge
particle.charge *= 0.99;
// Check if particle is within extended bounds
if (
particle.x >= minX && particle.x <= maxX &&
particle.y >= minY && particle.y <= maxY
) {
// If outside screen but within overscan, keep it if we need more particles
if (
(particle.x < 0 || particle.x > width ||
particle.y < 0 || particle.y > height) &&
newParticles.length >= targetParticleCount
) {
continue;
}
newParticles.push(particle);
} else {
// Out of bounds, respawn if needed
if (newParticles.length < targetParticleCount) {
newParticles.push(createParticleOnEdge());
}
}
}
// Add more particles if needed
while (newParticles.length < targetParticleCount) {
newParticles.push(createParticleOnEdge());
}
particles = newParticles;
}
// Render function
const mul = isStandalone ? 0.9 : 0.5;
const add = isStandalone ? 0.1 : 0.03;
function render(): void {
if (!ctx) return;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw particles
for (const particle of particles) {
// Only draw if within canvas bounds (plus a small margin)
if (
particle.x >= -PARTICLE_RADIUS &&
particle.x <= width + PARTICLE_RADIUS &&
particle.y >= -PARTICLE_RADIUS && particle.y <= height + PARTICLE_RADIUS
) {
ctx.beginPath();
ctx.arc(particle.x, particle.y, PARTICLE_RADIUS, 0, Math.PI * 2);
// Color based on charge
ctx.fillStyle = "#FFCB1F";
ctx.globalAlpha = (particle.charge * mul + add) * globalOpacity;
ctx.fill();
}
}
// // Debug: Draw forces and falloff visualization
// if (ctx) {
// for (const force of forces) {
// const R = force.radius * FORCE_RADIUS;
// // Draw force point
// ctx.beginPath();
// ctx.arc(force.x, force.y, 5, 0, Math.PI * 2);
// ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
// ctx.fill();
// // Draw force direction
// ctx.beginPath();
// ctx.moveTo(force.x, force.y);
// ctx.lineTo(force.x + force.dx * 20, force.y + force.dy * 20);
// ctx.strokeStyle = 'red';
// ctx.stroke();
// // Visualize the falloff curve with rings
// for (let i = 0; i <= 10; i++) {
// const radius = (R * i) / 10;
// const normalizedDist = radius / R;
// const intensity = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT);
// ctx.beginPath();
// ctx.arc(force.x, force.y, radius, 0, Math.PI * 2);
// ctx.strokeStyle = `rgba(255, 0, 0, ${intensity * 0.2})`;
// ctx.stroke();
// }
// }
// }
}
// Animation loop
let r = Math.random();
function animate(): void {
globalOpacity = Math.min(1, globalOpacity + 0.03);
update();
render();
if (isRunning) {
animationId = requestAnimationFrame(animate);
}
}
// Start/stop functions
function start(): void {
if (isRunning) return;
// Calculate target particle count based on canvas size
targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY);
// Clear any existing particles and create new ones with proper spacing
particles = [];
addParticles(targetParticleCount);
isRunning = true;
animate();
}
function stop(): void {
isRunning = false;
if (animationId !== null) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
init();
return cleanup;
};

View file

@ -0,0 +1,160 @@
(globalThis as any).canvas_2022 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
// Configuration for the grid of rotating squares
const config = {
gridRotation: 20, // Overall grid rotation in degrees
squareSize: 20, // Size of each square
spacing: 100, // Distance between square centers
moveSpeedX: 0.01, // Horizontal movement speed (pixels per second)
moveSpeedY: 0.01, // Vertical movement speed (pixels per second)
squareColor: "#00220A", // Color of the squares
squareOpacity: 1, // Opacity of the squares
// Function to determine square rotation based on its coordinates and time
// Can be adjusted for different patterns
rotationFunction: (x: number, y: number, time: number): number => {
// Combination of spatial wave and time-based rotation
return Math.sin(x * 0.05) * Math.cos(y * 0.05) * 180;
},
};
// Convert grid rotation to radians
const gridRotationRad = (config.gridRotation * Math.PI) / 180;
// Get the canvas context
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Could not get canvas context");
return () => {};
}
// Make canvas transparent
if (isStandalone) {
canvas.style.backgroundColor = "#154226";
} else {
canvas.style.backgroundColor = "transparent";
}
// Animation variables
let width = canvas.width;
let height = canvas.height;
let offsetX = 0;
let offsetY = 0;
let time = 0;
let animationFrameId: number;
let lastTime = 0;
// Update canvas dimensions when resized
const updateDimensions = () => {
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
};
// Calculate the diagonal length of the canvas (to ensure rotation covers corners)
const calculateDiagonal = () => {
return Math.sqrt(width * width + height * height);
};
// Draw a single square with rotation
const drawSquare = (x: number, y: number, size: number, rotation: number) => {
ctx.save();
// Move to the center of the square position, rotate, then draw
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180); // Convert rotation degrees to radians
// Draw square centered at position
ctx.fillRect(-size / 2, -size / 2, size, size);
ctx.restore();
};
// Draw the entire grid of squares
const drawGrid = () => {
ctx.clearRect(0, 0, width, height);
// Set drawing properties
ctx.fillStyle = config.squareColor;
ctx.globalAlpha = config.squareOpacity;
// Save the current transformation state
ctx.save();
// Move to the center of the canvas, rotate the grid, then move back
const centerX = width / 2;
const centerY = height / 2;
ctx.translate(centerX, centerY);
ctx.rotate(gridRotationRad);
// Calculate how much of the grid to draw based on canvas size
const diagonal = calculateDiagonal();
const gridSize = Math.ceil(diagonal / config.spacing) + 2;
// Adjust for offset to create movement
const adjustedOffsetX = offsetX % config.spacing;
const adjustedOffsetY = offsetY % config.spacing;
// Draw grid with enough squares to cover the rotated canvas
const halfGrid = Math.ceil(gridSize / 2);
for (let y = -halfGrid; y <= halfGrid; y++) {
for (let x = -halfGrid; x <= halfGrid; x++) {
// Calculate actual position with offset
const posX = x * config.spacing + adjustedOffsetX;
const posY = y * config.spacing + adjustedOffsetY;
// Calculate square rotation based on its position and time
const squareRotation = config.rotationFunction(posX, posY, time);
// Draw the square
drawSquare(posX, posY, config.squareSize, squareRotation);
}
}
// Restore the transformation state
ctx.restore();
// Reset global alpha
ctx.globalAlpha = 1.0;
};
// Animation loop
const animate = (currentTime: number) => {
animationFrameId = requestAnimationFrame(animate);
// Calculate time elapsed since last frame
const elapsed = currentTime - lastTime;
lastTime = currentTime;
// Update time variable for rotation function
time += elapsed;
// Update position offsets for movement
offsetX += config.moveSpeedX * elapsed;
offsetY += config.moveSpeedY * elapsed;
// Draw the grid
drawGrid();
};
// Initialize the animation
const init = () => {
// Set up resize handler
window.addEventListener("resize", updateDimensions);
// Initial setup
updateDimensions();
// Start animation
animationFrameId = requestAnimationFrame(animate);
};
// Start the animation
init();
// Return cleanup function
return () => {
window.removeEventListener("resize", updateDimensions);
cancelAnimationFrame(animationFrameId);
};
};

View file

@ -0,0 +1,197 @@
(globalThis as any).canvas_2023 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
const config = {
heartBaseSize: 50,
heartMaxSize: 100,
spacing: 150,
rowSpeed: 0.1,
heartColor: "#FF90D9",
heartOpacity: isStandalone ? 0.5 : 0.04,
mouseInfluenceRadius: 1000,
heartScaleFunction: (distance: number, radius: number): number => {
if (distance > radius) return 1;
const normalizedDistance = distance / radius;
const scaleFactor = 1 +
(1 - normalizedDistance) *
(config.heartMaxSize / config.heartBaseSize - 1);
return 1 + (scaleFactor - 1) * Math.pow(1 - normalizedDistance, 2);
},
};
const heart = new Path2D(
"M23.9451 45.3973L20.8672 42.6493C16.9551 39.0174 13.7054 35.8927 11.1181 33.275C8.53056 30.6574 6.46731 28.286 4.92839 26.1608C3.38946 24.0356 2.31772 22.1028 1.71314 20.3624C1.10856 18.6219 0.806274 16.8705 0.806274 15.1081C0.806274 11.4718 2.03118 8.42016 4.481 5.95312C6.93118 3.48608 9.93831 2.25256 13.5024 2.25256C15.5649 2.25256 17.482 2.70142 19.2536 3.59912C21.0255 4.49682 22.5893 5.80674 23.9451 7.52887C25.484 5.73346 27.1059 4.40522 28.8108 3.54416C30.5161 2.6831 32.3751 2.25256 34.3877 2.25256C38.0141 2.25256 41.0551 3.48663 43.5108 5.95477C45.9661 8.42291 47.1938 11.4758 47.1938 15.1136C47.1938 16.8712 46.8823 18.6115 46.2594 20.3343C45.6365 22.0568 44.5648 23.9807 43.0442 26.1059C41.5236 28.231 39.4721 30.6136 36.8896 33.2536C34.3068 35.8936 31.0362 39.0255 27.0779 42.6493L23.9451 45.3973ZM23.9176 38.802C27.6088 35.431 30.6339 32.5547 32.9928 30.173C35.3518 27.7913 37.2091 25.7211 38.5648 23.9624C39.9205 22.2036 40.864 20.6137 41.3953 19.1928C41.9266 17.7715 42.1923 16.4101 42.1923 15.1086C42.1923 12.8768 41.4529 11.0098 39.974 9.50748C38.4952 8.0052 36.6461 7.25406 34.4268 7.25406C32.631 7.25406 30.9572 7.6811 29.4055 8.87193C27.8537 10.0628 25.5389 13.0434 25.5389 13.0434L23.9451 15.3299L22.3512 13.0434C22.3512 13.0434 20.0643 10.2311 18.4638 9.04031C16.8634 7.84948 15.2194 7.25406 13.4991 7.25406C11.2929 7.25406 9.46857 7.98816 8.02602 9.45637C6.58383 10.9246 5.86273 12.8162 5.86273 15.1311C5.86273 16.4784 6.13644 17.8679 6.68386 19.2994C7.23127 20.731 8.18394 22.3333 9.54185 24.1064C10.8998 25.879 12.7329 27.9562 15.0413 30.3379C17.3497 32.7196 20.3084 35.5409 23.9176 38.802Z",
);
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Could not get canvas context");
return () => {};
}
if (isStandalone) {
canvas.style.backgroundColor = "#2F1C21";
} else {
canvas.style.backgroundColor = "transparent";
}
let width = canvas.width;
let height = canvas.height;
let animationFrameId: number;
let lastFrameTime = 0;
let mouseX = width / 2;
let mouseY = height / 2;
let offset = config.spacing / 2;
const updateDimensions = () => {
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
mouseX = width / 2;
mouseY = height / 2;
};
const drawHeart = (x: number, y: number, size: number) => {
const scale = size / 30;
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.fillStyle = config.heartColor;
ctx.fill(heart);
ctx.restore();
};
const c = 400;
const h = 40;
const k = solveForK(c, h);
const drawHeartGrid = () => {
ctx.clearRect(0, 0, width, height);
ctx.globalAlpha = config.heartOpacity;
const numRows = Math.ceil(height / config.spacing) + 1;
for (let row = 0; row < numRows; row++) {
const direction = row % 2 === 0 ? 1 : -1;
const rowOffset = (offset * direction) % config.spacing;
const posYInit = row * config.spacing + config.spacing / 2;
for (
let posXInit = -config.spacing + rowOffset;
posXInit < width + config.spacing;
posXInit += config.spacing
) {
const dx = (posXInit + config.heartBaseSize / 2) - mouseX;
const dy = (posYInit + config.heartBaseSize / 2) - mouseY;
const distance = Math.sqrt(dx * dx + dy * dy);
const pushIntensity = asymmetricBump(distance, h, c, k, 0.00002);
const pushAngle = Math.atan2(dy, dx);
const pushDistanceX = pushIntensity * Math.cos(pushAngle);
const pushDistanceY = pushIntensity * Math.sin(pushAngle);
const posX = posXInit + pushDistanceX * 1;
const posY = posYInit + pushDistanceY * 2;
const scaleFactor = config.heartScaleFunction(
distance,
config.mouseInfluenceRadius,
);
const heartSize = config.heartBaseSize * scaleFactor;
if (
posX > -config.heartMaxSize &&
posX < width + config.heartMaxSize &&
posY > -config.heartMaxSize &&
posY < height + config.heartMaxSize
) {
drawHeart(posX - heartSize / 2, posY - heartSize / 2, heartSize);
}
}
}
ctx.globalAlpha = 1.0;
};
function solveForK(c: number, k: number) {
// input -> f(x)=h*e^{(-k*(x-c)^{2})}
// desired result is (0, 0.45). (0, 0) is unsolvable but 0.45px will round down to 0.
//
// solution: -\frac{\ln\left(\frac{0.45}{h}\right)}{c^{2}}
return -Math.log(0.45 / h) / (c * c);
}
function asymmetricBump(
x: number,
h: number,
c: number,
leftK: number,
rightK: number,
) {
const k = (x <= c) ? leftK : rightK;
return h * Math.exp(-k * Math.pow(x - c, 2));
}
const updateOffset = (elapsed: number) => {
offset += config.rowSpeed * elapsed;
if (offset > 1000000) {
offset -= 1000000;
}
};
const animate = (currentTime: number) => {
animationFrameId = requestAnimationFrame(animate);
const elapsed = currentTime - lastFrameTime;
lastFrameTime = currentTime;
updateOffset(elapsed * 0.05);
drawHeartGrid();
};
const handleMouseMove = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouseX = event.clientX - rect.left;
mouseY = event.clientY - rect.top;
};
const handleTouchMove = (event: TouchEvent) => {
if (event.touches.length > 0) {
event.preventDefault();
const rect = canvas.getBoundingClientRect();
mouseX = event.touches[0].clientX - rect.left;
mouseY = event.touches[0].clientY - rect.top;
}
};
const init = () => {
window.addEventListener("resize", updateDimensions);
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("touchmove", handleTouchMove, { passive: false });
updateDimensions();
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
};
init();
return () => {
window.removeEventListener("resize", updateDimensions);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("touchmove", handleTouchMove);
cancelAnimationFrame(animationFrameId);
};
};

View file

@ -0,0 +1,251 @@
// Vibe coded with AI
(globalThis as any).canvas_2024 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
if (isStandalone) {
canvas.parentElement!.style.backgroundColor = "black";
}
const gl = canvas.getContext("webgl", {
alpha: true,
premultipliedAlpha: false,
});
if (!gl) {
console.error("WebGL not supported");
return () => {};
}
canvas.style.imageRendering = "pixelated";
canvas.style.opacity = isStandalone ? "0.3" : "0.15";
// Resize canvas to match display size
const resize = () => {
const displayWidth = Math.floor(
(canvas.clientWidth || window.innerWidth) / 3,
);
const displayHeight = Math.floor(
(canvas.clientHeight || window.innerHeight) / 3,
);
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
};
resize();
// Vertex shader (just passes coordinates)
const vertexShaderSource = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader creates random noise with higher opacity to ensure visibility
const fragmentShaderSource = `
precision mediump float;
uniform float u_time;
float noise1(float seed1,float seed2){
return(
fract(seed1+12.34567*
fract(100.*(abs(seed1*0.91)+seed2+94.68)*
fract((abs(seed2*0.41)+45.46)*
fract((abs(seed2)+757.21)*
fract(seed1*0.0171))))))
* 1.0038 - 0.00185;
}
float n(float seed1, float seed2, float seed3){
float buff1 = abs(seed1+100.81) + 1000.3;
float buff2 = abs(seed2+100.45) + 1000.2;
float buff3 = abs(noise1(seed1, seed2)+seed3) + 1000.1;
buff1 = (buff3*fract(buff2*fract(buff1*fract(buff2*0.146))));
buff2 = (buff2*fract(buff2*fract(buff1+buff2*fract(buff3*0.52))));
buff1 = noise1(buff1, buff2);
return(buff1);
}
void main() {
float noise = n(gl_FragCoord.x, gl_FragCoord.y, u_time);
gl_FragColor = vec4(1.0, 0.7, 0.7, 0.8*noise);
}
`;
// Create and compile shaders
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
);
// Check if shader creation failed
if (!vertexShader || !fragmentShader) {
console.error("Failed to create shaders");
return () => {};
}
// Create program and link shaders
const program = createProgram(gl, vertexShader, fragmentShader);
// Check if program creation failed
if (!program) {
console.error("Failed to create program");
return () => {};
}
// Get attribute and uniform locations
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const timeUniformLocation = gl.getUniformLocation(program, "u_time");
// Create a position buffer for a rectangle covering the entire canvas
const positionBuffer = gl.createBuffer();
if (!positionBuffer) {
console.error("Failed to create position buffer");
return () => {};
}
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Rectangle that covers the entire clip space
const positions = [
-1.0,
-1.0, // bottom left
1.0,
-1.0, // bottom right
-1.0,
1.0, // top left
1.0,
1.0, // top right
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Set up blending
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Fixed 24 FPS timing
const FPS = 24;
const FRAME_TIME = 1000 / FPS; // ms per frame
// Handle animation
let animationTimerId: number;
let startTime = Date.now();
let lastFrameTime = 0;
const render = () => {
// Get current time
const currentTime = Date.now();
const deltaTime = currentTime - lastFrameTime;
// Skip frame if it's too early (maintain 24 FPS)
if (deltaTime < FRAME_TIME) {
animationTimerId = window.setTimeout(render, 0); // Check again ASAP but yield to browser
return;
}
// Update last frame time, accounting for any drift
lastFrameTime = currentTime - (deltaTime % FRAME_TIME);
// Resize canvas if needed
resize();
// Calculate elapsed time in seconds for animation
const elapsedTime = (currentTime - startTime) / 1000;
// Clear the canvas with transparent black
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Use our shader program
gl.useProgram(program);
// Set up the position attribute
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(
positionAttributeLocation,
2, // 2 components per vertex
gl.FLOAT, // data type
false, // normalize
0, // stride (0 = compute from size and type)
0, // offset
);
// Update time uniform for animation
gl.uniform1f(timeUniformLocation, elapsedTime);
// Draw the rectangle (2 triangles)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Schedule next frame (aiming for 24 FPS)
const timeToNextFrame = Math.max(
0,
FRAME_TIME - (Date.now() - currentTime),
);
animationTimerId = window.setTimeout(render, timeToNextFrame);
};
// Helper function to create shaders
function createShader(
gl: WebGLRenderingContext,
type: number,
source: string,
): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) {
console.error("Failed to create shader object");
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compilation error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Helper function to create program and link shaders
function createProgram(
gl: WebGLRenderingContext,
vertexShader: WebGLShader,
fragmentShader: WebGLShader,
): WebGLProgram | null {
const program = gl.createProgram();
if (!program) {
console.error("Failed to create program object");
return null;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program linking error:", gl.getProgramInfoLog(program));
return null;
}
return program;
}
// Start the rendering with initial timestamp
lastFrameTime = Date.now();
render();
// Return cleanup function
return () => {
clearTimeout(animationTimerId);
if (program) gl.deleteProgram(program);
if (vertexShader) gl.deleteShader(vertexShader);
if (fragmentShader) gl.deleteShader(fragmentShader);
if (positionBuffer) gl.deleteBuffer(positionBuffer);
};
};

View file

@ -0,0 +1,362 @@
// @ts-ignore
globalThis.canvas_cotyledon = function (
canvas: HTMLCanvasElement,
panel: HTMLElement,
) {
let running = true;
const ctx = canvas.getContext("2d");
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
const clover = new Path2D(
"M18.9845 34.4839C20.4004 34.5218 21.8336 34.6883 23.2644 34.9578C20.1378 31.095 18.4268 27.1546 18.0555 23.2959C17.321 15.6622 21.9022 9.36595 28.8908 5.78535C34.6355 2.84212 40.258 2.98454 44.2809 5.96879C45.6605 6.99221 46.7683 8.2886 47.5877 9.78593C48.3054 8.50307 49.134 7.26623 50.0858 6.17951C51.8368 4.18037 54.1947 2.47127 57.2294 2.15019C60.2768 1.82766 63.467 2.9608 66.7548 5.52299C70.9834 8.81811 73.084 12.8864 73.5996 17.2135C74.1044 21.4504 73.0711 25.7433 71.4155 29.6117C70.6566 31.3849 69.7488 33.1106 68.7557 34.7506C70.3664 33.9983 72.0168 33.3376 73.6816 32.8312C77.2262 31.7528 81.0258 31.3024 84.8151 32.2149C88.6451 33.1371 92.2246 35.3946 95.3823 39.3157C98.4534 43.1293 99.9219 46.6818 99.997 49.9677C100.073 53.3033 98.7051 55.9829 96.8652 57.9789C95.0586 59.9387 92.7653 61.2872 90.7505 62.1315C90.692 62.1561 90.6334 62.1802 90.5746 62.2042L90.4465 62.256C91.4852 63.7304 92.4724 65.5955 93.0127 67.6979C93.5916 69.9509 93.6669 72.5285 92.674 75.1356C91.679 77.7482 89.7006 80.1559 86.5767 82.2161C86.5556 82.23 86.5342 82.2438 86.5126 82.2571C84.1333 83.7267 81.5504 84.7197 78.6932 84.9352C75.832 85.151 72.8634 84.5742 69.7337 83.1522C64.7667 80.8953 59.274 76.4525 52.8745 69.3645C52.8789 70.1568 52.8844 70.9254 52.9004 71.6677C52.9643 74.6226 53.1868 77.4534 54.0666 80.6265C55.2259 84.503 57.2821 88.4684 60.9561 92.3161C61.644 93.0366 61.8512 94.0908 61.4872 95.018L60.9919 96.2799C60.6464 97.16 59.8435 97.778 58.9041 97.8865C57.9647 97.9952 57.042 97.5769 56.5047 96.7985C52.5406 91.0574 50.3441 86.3289 49.1491 82.0434C48.0155 78.2319 47.6244 74.4579 47.5085 71.0024C45.418 73.6873 42.8696 76.4687 40.0618 78.9101C34.3517 83.8756 26.6803 88.1931 19.142 85.9955C15.5301 84.9425 12.8635 83.2751 11.0848 81.1179C9.2952 78.9474 8.5557 76.4627 8.4981 74.0631C8.43961 71.6256 9.07998 69.225 10.075 67.1703C7.76333 66.828 5.38011 65.9682 3.47071 64.2327C-0.339092 60.7699 -1.2199 54.8876 1.86982 46.4552C3.47011 42.0878 5.90372 38.9798 8.98328 37.0179C12.0444 35.0677 15.5215 34.3912 18.9845 34.4839Z",
);
// Background
const base = [0x14, 0x1a, 0x19];
let blobTextureCanvas: OffscreenCanvas;
let blobTextureCtx: OffscreenCanvasRenderingContext2D;
const blobSize = 1000; // Size of the noise texture
{
const blobTexture = new ImageData(blobSize, blobSize);
const data = blobTexture.data;
let x = 0;
let y = 0;
for (let i = 0; i < data.length; i += 4) {
if (x >= blobSize) {
x = 0;
y++;
}
const noiseX = Math.sin(x * .2 + y * .1) * 0.03;
const noiseY = Math.cos(y * .6 + x * .2) * 0.05;
const centerX = blobSize / 2;
const centerY = blobSize / 2;
const dx = (x + noiseX) - centerX;
const dy = (y + noiseY) - centerY;
const distanceFromCenterRaw = (dx * dx + dy * dy) ** 0.5;
const maxDistance = blobSize / 2;
const distanceFromCenter = Math.min(
1,
distanceFromCenterRaw / maxDistance,
);
const noiseValue = (1 - 0.5 * Math.sin(x * 0.02 - y * 0.04)) *
(1 - 0.5 * Math.cos(x * 0.03 + y * 0.04)) * 0.3;
const gradient = (1 - distanceFromCenter) *
(0.95 - distanceFromCenter * 0.4);
const finalValue = Math.max(
0,
Math.min(1, gradient * (Math.random() * 0.3 + 0.85 + noiseValue)),
);
data[i] = 121;
data[i + 1] = 219;
data[i + 2] = 160;
data[i + 3] = Math.floor(finalValue * 255.99); // Alpha
x++;
}
blobTextureCanvas = new OffscreenCanvas(blobSize, blobSize);
blobTextureCtx = blobTextureCanvas.getContext("2d")!;
blobTextureCtx.putImageData(blobTexture, 0, 0);
}
class CotyledonParticle {
x: number;
y: number;
size: number;
velocityX: number;
velocityY: number;
rotation: number;
rotationSpeed: number;
color: string;
constructor(positioning: "random" | "edge") {
this.size = Math.random() * 0.1 + 0.6;
if (positioning === "edge") {
const edge = Math.floor(Math.random() * 5);
if (edge === 0 || edge === 1) {
// Right edge
this.x = 1.05;
this.y = Math.random();
this.velocityX = -Math.random() * 0.1 - 0.2;
this.velocityY = 0;
} else if (edge === 2 || edge === 3) {
// Top edge
this.x = Math.random();
this.y = -0.05;
this.velocityX = -Math.random() * 0.1 - 0.1;
this.velocityY = -Math.random() * 0.2 - 0.05;
} else {
// Bottom edge
this.x = Math.random() * 0.5 + 0.5;
this.y = 1.05;
this.velocityX = Math.random() * 0.1 + 0.1;
this.velocityY = -Math.random() * 0.3 - 0.1;
}
} else {
let tries = 0;
do {
this.x = Math.random();
this.y = Math.random();
this.velocityX = -Math.random() * 0.05 - 0.1;
this.velocityY = -Math.random() * 0.2 + 0.1;
} while (this.tooCloseToAnyOtherParticle() && (tries++ < 10));
}
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() * 0.003 - 0.0015) *
(Math.random() > 0.5 ? 1 : -1);
const opacity = Math.random() * 0.4 + 0.2;
this.color = `rgba(${
base.map((x) => x + Math.floor(x * opacity)).join(",")
}, 1)`;
}
tooCloseToAnyOtherParticle() {
for (let i = 0; i < cotyledonParticles.length; i++) {
const otherParticle = cotyledonParticles[i];
const distance = Math.sqrt(
(this.x - otherParticle.x) ** 2 + (this.y - otherParticle.y) ** 2,
);
if (distance < 0.1) {
return true;
}
}
}
update() {
if (this.velocityY < 0.01) {
this.velocityY += 0.00025;
}
this.velocityX -= 0.0001;
this.x += this.velocityX / 1300;
this.y += this.velocityY / 1000;
this.rotation += this.rotationSpeed;
return this.x < -0.05 || (this.y > 1 && this.velocityY < 0);
}
draw() {
if (!ctx) return;
ctx.save();
ctx.translate(this.x * canvas.width, this.y * canvas.height);
ctx.rotate(this.rotation);
ctx.scale(this.size, this.size);
ctx.translate(-50, -50);
ctx.fillStyle = this.color;
ctx.fill(clover);
ctx.restore();
}
}
class BlobParticle {
x: number;
y: number;
opacity: number;
state: 0 | 1 | 2;
stateTime: number;
stayDuration: number;
innerColor: string;
rot: number = Math.random() * Math.PI * 2;
constructor() {
this.x = Math.random() * 0.6 + 0.2;
this.y = Math.random() * 0.6 + 0.2;
this.opacity = 0; // Start fully transparent
this.state = 0;
this.stateTime = 0;
this.stayDuration = Math.random() * 10000 + 5000; // Random stay duration between 5-15 seconds
const colorMultiplier = Math.random() * 0.5 + 1.5; // 0.5-1.0 multiplier
const colorValues = base.map((x) => Math.floor(x * colorMultiplier));
this.innerColor = `rgba(${colorValues.join(",")}, 1)`;
}
update(deltaTime: number) {
this.stateTime += deltaTime;
if (this.state === 0) {
this.opacity = Math.min(1, this.stateTime / 15000);
if (this.stateTime >= 15000) {
this.state = 1;
this.stateTime = 0;
}
} else if (this.state === 1) {
if (this.stateTime >= this.stayDuration) {
this.state = 2;
this.stateTime = 0;
}
} else if (this.state === 2) {
this.opacity = Math.max(0, 1 - (this.stateTime / 15000));
if (this.stateTime >= 15000) {
return true;
}
}
return false;
}
draw() {
if (!ctx) return;
const screenX = this.x * canvas.width;
const screenY = this.y * canvas.height;
const screenSize = (Math.min(canvas.width, canvas.height) * 2) / blobSize;
ctx.save();
ctx.translate(screenX - screenSize * 0.5, screenY - screenSize * 0.5);
ctx.scale(screenSize, screenSize);
ctx.rotate(this.rot);
ctx.globalAlpha = this.opacity * 0.2;
ctx.globalCompositeOperation = "overlay";
ctx.drawImage(blobTextureCanvas, 0, 0);
ctx.restore();
}
}
const cotyledonParticles: CotyledonParticle[] = [];
const blobParticles: BlobParticle[] = [];
let blobParticleTop: BlobParticle = new BlobParticle();
for (let i = 0; i < 80; i++) {
cotyledonParticles.push(new CotyledonParticle("random"));
}
for (let i = 0; i < 9; i++) {
const blobParticle = new BlobParticle();
if (i < 4) {
blobParticle.state = 1;
blobParticle.opacity = 1;
blobParticle.stayDuration = Math.random() * 10000;
} else {
blobParticle.state = i < 7 ? 2 : 0;
blobParticle.stateTime = Math.random() * 15000;
}
if (i > 0) {
do {
blobParticle.x = Math.random();
blobParticle.y = Math.random();
} while (
blobParticles.some((p) =>
Math.sqrt((p.x - blobParticle.x) ** 2 + (p.y - blobParticle.y) ** 2) <
0.1
)
);
}
blobParticles.push(blobParticle);
}
let lastTime = performance.now();
function animate(currentTime: number) {
if (!running) return;
if (!ctx) return;
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = blobParticles.length - 1; i >= 0; i--) {
const shouldRemove = blobParticles[i].update(deltaTime);
if (shouldRemove) {
blobParticles[i] = new BlobParticle();
} else {
blobParticles[i].draw();
}
}
for (let i = cotyledonParticles.length - 1; i >= 0; i--) {
const shouldRemove = cotyledonParticles[i].update();
if (shouldRemove) {
cotyledonParticles[i] = new CotyledonParticle("edge");
} else {
cotyledonParticles[i].draw();
}
}
if (blobParticleTop.update(deltaTime)) {
blobParticleTop = new BlobParticle();
}
blobParticleTop.draw();
requestAnimationFrame(animate);
}
let clickedButton = false;
const enterButton = panel.querySelector("button#enter")!;
enterButton.addEventListener("click", () => {
if (clickedButton) return;
clickedButton = true;
const first = panel.querySelector("#first")! as HTMLElement;
const second = panel.querySelector("#captcha")! as HTMLElement;
first.style.transition = second.style.transition = "opacity 1s ease-in-out";
first.style.opacity = "0";
second.style.opacity = "0";
setTimeout(() => {
first.style.display = "none";
second.style.display = "block";
setTimeout(() => {
second.style.opacity = "1";
document.getElementById("enter2")?.addEventListener("click", () => {
second.style.opacity = "0";
let p = fetch("/file/cotyledon", {
method: "POST",
body: "I AGREE",
});
setTimeout(() => {
p.then(() => {
location.reload();
});
}, 1000);
});
}, 10);
}, 1000);
});
const imageButtons = panel.querySelectorAll(".image-grid button")!;
imageButtons.forEach((button) => {
let canClick = true;
button.addEventListener("click", () => {
if (!canClick) return;
canClick = false;
const image = button.querySelector("img")!;
image.style.transition = "opacity 0.05s linear";
image.style.opacity = "0";
setTimeout(() => {
image.style.transition = "opacity 2s linear";
let newNum;
do {
newNum = Math.floor(Math.random() * 18); // 0-17 inclusive
} while (
document.querySelector(`img[src="/captcha/image/${newNum}.jpeg"]`)
);
image.setAttribute("src", `/captcha/image/${newNum}.jpeg`);
setTimeout(() => {
image.style.opacity = "0.75";
canClick = true;
}, 50);
}, 300);
});
});
// Start animation
animate(performance.now());
return () => {
window.removeEventListener("resize", resizeCanvas);
running = false;
};
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -0,0 +1,12 @@
---
import { useInlineScript } from "../framework/page-resources.ts";
const { script } = Astro.props;
useInlineScript('canvas_' + script as any);
useInlineScript('canvas_demo');
---
<canvas id="canvas"
data-canvas={script}
data-standalone="true"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;"
></canvas>

View file

@ -0,0 +1,10 @@
const canvas = document.querySelector("canvas");
const id = canvas?.getAttribute("data-canvas");
if (!id) {
throw new Error("No canvas id found");
}
const func = (window as any)["canvas_" + id];
if (!func) {
throw new Error("No canvas function found");
}
func(canvas, document.body);

View file

@ -0,0 +1,685 @@
const filesContainer = document.querySelector(".files")!;
// push the scrollbar to the end of the view.
let distanceFromEnd = 0;
let fakeScrollToEnd: number | null = null;
filesContainer.scrollLeft = filesContainer.scrollWidth -
filesContainer.clientWidth;
window.addEventListener("resize", () => {
if (fakeScrollToEnd) return;
const { scrollWidth, clientWidth } = filesContainer;
if (scrollWidth <= clientWidth) {
distanceFromEnd = 0;
} else {
filesContainer.scrollLeft = scrollWidth - clientWidth - distanceFromEnd;
}
}, { passive: false });
let lastScrollLeft = filesContainer.scrollLeft;
filesContainer.addEventListener("scroll", (ev) => {
const { scrollWidth, scrollLeft, clientWidth } = filesContainer;
if (scrollWidth <= clientWidth) {
distanceFromEnd = 0;
} else {
distanceFromEnd = scrollWidth - scrollLeft - clientWidth;
if (fakeScrollToEnd && scrollLeft < lastScrollLeft) {
cancelAnimationFrame(fakeScrollToEnd);
fakeScrollToEnd = null;
}
}
lastScrollLeft = scrollLeft;
});
const lerp = (a: number, b: number, t: number) => a + t * (b - a);
function snapToEnd(initScrollStart: number) {
if (fakeScrollToEnd) {
cancelAnimationFrame(fakeScrollToEnd);
}
let lastTime = performance.now();
let scrollStart = initScrollStart ?? filesContainer.scrollLeft;
if (scrollStart >= filesContainer.scrollWidth) {
return;
}
fakeScrollToEnd = requestAnimationFrame(function tick() {
const now = performance.now();
const dt = now - lastTime;
lastTime = now;
const f = 1 - (0.98 ** dt);
const { scrollWidth, clientWidth } = filesContainer;
const target = Math.floor(scrollWidth - clientWidth);
scrollStart = lerp(scrollStart, target, f);
filesContainer.scrollLeft = scrollStart;
if (Math.abs(scrollStart - target) < 0.2) {
fakeScrollToEnd = null;
} else {
fakeScrollToEnd = requestAnimationFrame(tick);
}
});
}
interface CacheEntry {
html: string;
expires: number;
}
// It is intentional that the existing page is NOT put into the cache. This is
// just to avoid the differences between the partials and the full page
// (subtle differences in activeFilename & isLast)
let currentFile: string = location.pathname.replace(/^\/file/, "");
let navigationId = 0;
const cache = new Map<string, CacheEntry>();
const prefetching = new Map<string, Promise<CacheEntry>>();
const fetchLater: string[] = [];
let hasCotyledonSpeedbump = false;
function prefetchEntry(
filePath: string,
lazy = false,
): void | Promise<CacheEntry> {
console.assert(filePath[0] === "/", "filePath must start with a /");
const existingEntry = cache.get(filePath);
if (existingEntry) {
if (existingEntry.expires > Date.now()) {
return;
}
cache.delete(filePath);
}
const existingPromise = prefetching.get(filePath);
if (existingPromise) return existingPromise;
// lazy prefetches should be limited
if (lazy && prefetching.size > 2) {
if (!fetchLater.includes(filePath)) {
fetchLater.push(filePath);
}
return;
}
if (filePath === "/cotyledon") {
ensureCanvasReady("cotyledon");
}
const promise = fetch(`/file${filePath}$partial`)
.then((resp) => {
if (resp.status !== 200) {
throw new Error(`Failed to fetch ${filePath}`);
}
return resp.text();
})
.then((html) => {
const entry: CacheEntry = { html, expires: Date.now() + 1000 * 60 * 20 };
cache.set(filePath, entry);
prefetching.delete(filePath);
if (fetchLater.length > 0 && prefetching.size < 2) {
const filePath = fetchLater.shift()!;
prefetchEntry(filePath, false);
}
return entry;
});
prefetching.set(filePath, promise);
return promise;
}
function fetchEntry(filePath: string): Promise<CacheEntry> {
const pf = prefetchEntry(filePath);
if (pf) return pf;
return Promise.resolve(cache.get(filePath)!);
}
type CanvasFn = (canvas: HTMLCanvasElement, panel: HTMLElement) => void;
const fetchCanvas = new Map<string, Promise<CanvasFn>>();
function ensureCanvasReady(id: string): Promise<CanvasFn> {
let func = (globalThis as any)["canvas_" + id];
if (func) return Promise.resolve(func);
let promise = fetchCanvas.get(id);
if (promise) return promise;
let resolve: (c: CanvasFn) => void;
promise = new Promise<CanvasFn>((r) => resolve = r);
fetchCanvas.set(id, promise);
const script = document.createElement("script");
script.src = `/js/canvas/${id}.js`;
script.async = true;
script.onload = () => {
func = (globalThis as any)["canvas_" + id];
fetchCanvas.delete(id);
if (func) {
resolve(func);
} else {
console.error(`Error loading canvas script: ${id}`);
}
};
script.onerror = () => {
console.error(`Error loading canvas script: ${id}`);
};
document.head.appendChild(script);
return promise;
}
interface Tooltip {
tooltip: HTMLElement;
top: number;
index: number;
}
const panels: Panel[] = [];
class Panel {
index: number;
panel: HTMLElement;
content: HTMLElement;
width: number;
tooltips: Tooltip[] | null;
linkFlags: number[] | null = null;
basenames: string[] | null = null;
unmountCanvas: (() => void) | null = null;
constructor(panel: HTMLElement, index: number) {
console.assert(panel.classList.contains("panel"));
this.panel = panel;
this.index = index;
this.content = panel.querySelector(".content.primary")!;
if (index === 0) {
this.panel.classList.add("first");
}
const canvas = panel.querySelector(
"canvas[data-canvas]",
) as HTMLCanvasElement;
if (canvas) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
requestAnimationFrame(() => {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
});
const id = canvas.getAttribute("data-canvas")!;
let cancelled = false;
this.unmountCanvas = () => (cancelled = true);
ensureCanvasReady(id).then((func) => {
if (cancelled) return;
this.unmountCanvas = func(canvas, panel) as any;
});
if (id === "cotyledon") {
filesContainer.classList.add("ctld-sb");
const group = document.querySelector(
"[data-group='cotyledon']",
)! as HTMLElement;
if (group) {
group.setAttribute("inert", "true");
group.style.opacity = "0.5";
group.style.pointerEvents = "none";
hasCotyledonSpeedbump = true;
}
}
}
const ul = panel.querySelector("ul");
if (!ul) {
this.width = 0;
this.linkFlags = null;
this.basenames = null;
this.tooltips = null;
return;
}
this.width = ul.offsetWidth;
const links = panel.querySelectorAll("ul > li > a.li");
this.content.setAttribute("data-clover", `${index}`);
this.tooltips = [];
this.linkFlags = new Array(links.length).fill(0);
const basenames = this.basenames = new Array(links.length);
for (let i = 0; i < links.length; i++) {
const link = links[i] as HTMLAnchorElement;
link.setAttribute("data-clover", `${index};${i}`);
link.addEventListener("mouseenter", onLinkMouseEnter);
basenames[i] = link.classList.contains("readme")
? "readme.txt"
: link.getAttribute("href")!.split("/").pop()!;
}
}
update(newActiveFile: string) {
console.assert(newActiveFile);
const p = this.panel;
p.querySelector(".li.active")?.classList.remove("active");
const basenames = this.basenames;
if (!basenames) return;
const ul = p.querySelector("ul")!;
this.width = ul.offsetWidth;
console.assert(!newActiveFile.includes("/"));
if (hasCotyledonSpeedbump) {
newActiveFile = "__";
}
const linkIndex = basenames.indexOf(newActiveFile);
if (linkIndex === -1) return;
const link = p.querySelector(`[data-clover="${this.index};${linkIndex}"]`)!;
link.classList.add("active");
const newActiveTooltip = this.tooltips!.findIndex((t) =>
t.index === linkIndex
);
for (let i = 0; i < this.tooltips!.length; i++) {
const { tooltip } = this.tooltips![i];
tooltip.classList[i === newActiveTooltip ? "add" : "remove"]("active");
}
}
hideReadme() {
this.panel.classList.remove("last");
const lastHsplit = this.panel.querySelector(".hsplit")!;
if (lastHsplit) {
lastHsplit.className = "hsplit-hidden";
const previousReadme = this.panel.querySelector(
".content.readme",
)! as HTMLElement;
console.assert(previousReadme, "No readme found");
previousReadme.style.display = "none";
}
}
showReadme() {
this.panel.classList.add("last");
const hsplit = this.panel.querySelector(".hsplit-hidden")!;
if (hsplit) {
hsplit.className = "hsplit";
const previousReadme = this.panel.querySelector(
".content.readme",
)! as HTMLElement;
console.assert(previousReadme, "No readme found");
previousReadme.style.display = "block";
}
}
destroy() {
if (this.unmountCanvas) {
this.unmountCanvas();
}
this.panel.querySelectorAll("audio,video").forEach((el) =>
(el as HTMLVideoElement | HTMLAudioElement).pause()
);
this.panel.remove();
}
}
function onContentScrollForTooltip(ev: Event) {
const content = ev.target as HTMLElement;
const panelIndex = parseInt(content.getAttribute("data-clover")!);
const panel = panels[panelIndex];
const scrollTop = content.scrollTop;
for (const tooltip of panel.tooltips!) {
tooltip.tooltip.style.transform = tooltipTransform(tooltip.top, scrollTop);
}
}
function tooltipTransform(offsetTop: number, scrollTop: number) {
return `translateY(${offsetTop - scrollTop}px)`;
}
let activeTooltip: HTMLElement | null = null;
let activeTooltipCancel: (() => void) | null = null;
function onLinkMouseEnter(e: MouseEvent) {
const link = e.target as HTMLAnchorElement;
console.assert(link.classList.contains("li"));
const attr = link.getAttribute("data-clover")!;
console.assert(attr && attr.match(/^\d+;\d+$/));
const [panelIndex, linkIndex] = attr.split(";").map(Number);
const panel = panels[panelIndex];
console.assert(panel, `panel${panelIndex}`);
const linkWidths = panel.linkFlags;
let filePath: string | null = null;
if (linkWidths![linkIndex] == 0) {
// filter only links that truncate their text
// insane discovery: while this is recommended online, it doesn't
// account for the `...` itself, meaning when just the file size
// is truncated, a tooltip won't be available.
// > if (link.scrollWidth <= link.offsetWidth) continue;
const lastChild = link.lastElementChild! as HTMLElement;
linkWidths![linkIndex] = lastChild?.offsetLeft !== undefined
? (lastChild.offsetLeft + lastChild.offsetWidth) ^ 0
: 1;
const href = (link as HTMLAnchorElement).getAttribute("href") ?? null;
filePath = href?.startsWith("/file") ? href.slice(5) || "/" : null;
}
if (filePath) {
prefetchEntry(filePath, true);
}
if (linkWidths![linkIndex] > panel.width) {
if (activeTooltipCancel) {
activeTooltipCancel();
activeTooltipCancel = null;
}
maybeBuildTooltipUi(link, linkIndex, panel);
}
}
function cancelOnMouseLeave() {
activeTooltipCancel!();
activeTooltipCancel = null;
}
function maybeBuildTooltipUi(
link: HTMLAnchorElement,
linkIndex: number,
panel: Panel,
) {
if (activeTooltip) {
activeTooltip.remove();
buildTooltipUi(link, linkIndex, panel, false);
} else {
link.addEventListener("mouseleave", cancelOnMouseLeave);
const timer = setTimeout(() => {
activeTooltipCancel = null;
link.removeEventListener("mouseleave", cancelOnMouseLeave);
buildTooltipUi(link, linkIndex, panel, true);
}, 150);
activeTooltipCancel = () => {
clearTimeout(timer);
link.removeEventListener("mouseleave", cancelOnMouseLeave);
};
}
}
function buildTooltipUi(
link: HTMLAnchorElement,
linkIndex: number,
panel: Panel,
animateIn: boolean,
) {
const tooltip = activeTooltip = document.createElement("div");
tooltip.classList.add("tooltip");
if (link.classList.contains("active")) {
tooltip.classList.add("active");
}
if (animateIn) {
tooltip.style.animation = "fadeIn .1s ease-out forwards";
}
tooltip.innerHTML = link.innerHTML;
const top = link.parentElement!.offsetTop;
tooltip.style.transform = tooltipTransform(top, panel.content.scrollTop);
panel.panel.appendChild(tooltip);
panel.tooltips!.push({ tooltip, top, index: linkIndex });
if (panel.tooltips!.length === 1) {
panel.content.addEventListener("scroll", onContentScrollForTooltip);
}
link.addEventListener("mouseleave", (e) => {
tooltip.style.animation = "fadeIn .3s .2s ease reverse forwards";
const timer = setTimeout(() => {
activeTooltipCancel = null;
tooltip.remove();
activeTooltip = null;
const tt = panel.tooltips = panel.tooltips!.filter((t) =>
t.tooltip !== tooltip
);
if (tt.length === 0) {
panel.content.removeEventListener("scroll", onContentScrollForTooltip);
}
}, 500);
activeTooltipCancel = () => {
clearTimeout(timer);
};
}, { once: true });
}
function addPanel(panel: HTMLElement, activeFile?: string) {
const p = new Panel(panel, panels.length);
panels.push(p);
if (activeFile) {
p.update(activeFile);
}
}
async function maybeInterceptClick(event: MouseEvent, element: HTMLElement) {
const href = (element as HTMLAnchorElement).href;
const url = new URL(href, window.location.origin);
if (maybeNavigate(url, true)) {
event.preventDefault();
}
}
type URLLike = Pick<URL, "pathname" | "origin" | "search">;
function maybeNavigate(url: URLLike, pushState: boolean) {
if (!url.pathname.startsWith("/file")) return false;
if (url.origin !== location.origin) return false;
if (url.search !== "") return false;
navigate(url.pathname, pushState);
return true;
}
async function navigate(pathname: string, pushState: boolean) {
const filePath = pathname.slice(5) || "/";
const currentNavigationId = ++navigationId;
if (filePath === currentFile) return;
const currentSplit = splitSlashes(currentFile);
const filePathSplit = splitSlashes(filePath);
// Find the first index where the currentSplit and filePathSplit differ, then
// add all the paths after that to panelsToFetch
let deleteCount = Math.max(currentSplit.length - filePathSplit.length, 0);
let appendPanels = [];
for (let i = -1; i < filePathSplit.length; i++) {
if (currentSplit[i] !== filePathSplit[i]) {
deleteCount = currentSplit.length - i;
appendPanels = [];
for (let j = i; j < filePathSplit.length; j++) {
appendPanels.push("/" + filePathSplit.slice(0, j + 1).join("/"));
}
break;
}
}
// Before fetching, prepare to mark the panels as loading
const loadingPanels = new Set<HTMLElement>();
{
let lastPanel = filesContainer.lastElementChild;
let toDelete = deleteCount;
while (lastPanel && toDelete > 0) {
lastPanel.querySelectorAll(".content").forEach((content) => {
loadingPanels.add(content as HTMLElement);
});
lastPanel = lastPanel.previousElementSibling;
toDelete--;
}
if (deleteCount == 0) {
const last = filesContainer.lastElementChild!;
console.assert(last, "Last panel is not a panel");
const readme = last.querySelector(".content.readme")!;
if (readme) {
loadingPanels.add(readme as HTMLElement);
}
}
const folderWithReadme = panels[panels.length - deleteCount - 1];
const readmes = folderWithReadme.panel.querySelectorAll(".readme");
if (readmes.length === 1) {
deleteCount += 1;
appendPanels.unshift(
"/" + currentSplit.slice(0, panels.length - deleteCount).join("/"),
);
}
}
console.assert(
deleteCount > 0 || appendPanels.length > 0,
"No difference found",
);
let timer = loadingPanels.size > 0
? setTimeout(() => {
if (navigationId !== currentNavigationId) {
return; // cancelled
}
document.querySelectorAll(".loading")
.forEach((thing) => thing.classList.remove("loading"));
for (const panel of loadingPanels) {
panel.classList.add("loading");
}
timer = null;
}, 100)
: null;
// Fetch the data
let appendEntries;
try {
appendEntries = await Promise.all(appendPanels.map(fetchEntry));
} catch (e) {
console.error("error", e);
if (navigationId === currentNavigationId) {
console.error(e);
location.href = "/file" + (filePath.length > 1 ? filePath : "");
}
return; // cancelled
}
if (navigationId !== currentNavigationId) {
return; // cancelled
}
if (timer) clearTimeout(timer);
else {for (const panel of loadingPanels) {
panel.classList.remove("loading");
}}
currentFile = filePath;
if (pushState) {
history.pushState(null, "", `/file${filePath.length > 1 ? filePath : ""}`);
}
const startScrollleft = filesContainer.scrollLeft;
if (currentSplit[0] !== filePathSplit[0]) {
if (currentSplit[0] === "cotyledon") {
filesContainer.classList.remove("ctld-et", "ctld-sb");
}
if (parseInt(currentSplit[0]) < 2025) {
filesContainer.classList.remove("ctld", "ctld-" + currentSplit[0]);
}
if (parseInt(filePathSplit[0]) < 2025) {
filesContainer.classList.add("ctld", "ctld-" + filePathSplit[0]);
}
}
// Make the last panel into a regular panel
panels[panels.length - 1].hideReadme();
// Delete the panels that are no longer needed
for (let i = 0; i < deleteCount; i++) {
const panel = panels.pop();
console.assert(panel, "No panel found");
if (panel) {
panel.destroy();
}
}
// Update the last panel
const currentFileSplit = splitSlashes(currentFile);
const activeFile = currentFileSplit[panels.length - 1];
panels[panels.length - 1]?.update(activeFile ?? "readme.txt");
// Insert the new panels
if (appendEntries.length > 0) {
let lastNewPanel: HTMLElement | null = null;
for (const entry of appendEntries) {
const panel = document.createElement("div");
panel.classList.add("panel");
if (panels.length >= 2) {
panel.classList.add("fade-slide-in");
}
panel.innerHTML = entry.html;
filesContainer.appendChild(panel);
lastNewPanel = panel;
const current = currentFileSplit[panels.length];
addPanel(panel, current ?? "readme.txt");
if (current) {
panels[panels.length - 1].hideReadme();
}
}
console.assert(lastNewPanel, "No last new panel found");
lastNewPanel!.classList.add("last");
// Automatically play videos
const video = lastNewPanel!.querySelector("video") ||
lastNewPanel!.querySelector("audio");
if (video) {
const timer = setTimeout(() => {
video.play();
}, 50);
video.play().then(() => {
clearTimeout(timer);
}, () => {});
}
} else {
// Make the last panel the .last panel
const lastPanel = filesContainer.lastElementChild!;
console.assert(lastPanel, "No last panel found");
lastPanel.classList.add("last");
panels[panels.length - 1].showReadme();
}
updateWidths();
filesContainer.scrollLeft = startScrollleft;
requestAnimationFrame(() => {
updateWidths();
filesContainer.scrollLeft = startScrollleft;
snapToEnd(startScrollleft);
});
}
function updateWidths() {
for (const panel of panels.slice(-2)) {
const ul = panel.panel.querySelector("ul")!;
if (ul) {
panel.width = ul.offsetWidth;
}
}
}
function splitSlashes(path: string) {
if (path.length <= 1) return [];
return path.slice(1).split("/");
}
requestAnimationFrame(() => {
document.querySelectorAll(".panel").forEach((panel) =>
addPanel(panel as HTMLElement)
);
(document.querySelector(".files")! as HTMLElement).addEventListener(
"click",
(event, element = event.target as HTMLAnchorElement) => {
if (
!(event.button ||
event.which != 1 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
event.defaultPrevented)
) {
while (element && element !== document.body) {
if (
element.nodeName.toUpperCase() === "A"
) {
maybeInterceptClick(event, element);
return;
}
element =
(element.assignedSlot ?? element.parentNode) as HTMLAnchorElement;
}
}
},
);
});
window.addEventListener("popstate", (event) => {
if (!maybeNavigate(window.location, false)) {
location.reload();
}
});

View file

@ -0,0 +1,798 @@
html, body {
height: 100vh;
width: 100vw;
overflow: hidden;
--dark-bg: #27143f;
--bar: #804fc9;
}
@font-face {
font-family: c1;
src: url(/cydn_header.woff2);
font-variation-settings: "wght" 50;
}
body {
display: grid;
background-color: var(--dark-bg);
}
.files {
background-color: var(--bg);
--scroll-bg: var(--dark-bg);
display: flex;
flex-direction: row;
overflow-x: scroll;
overflow-y: hidden;
min-height: 100vh;
border-bottom: 0;
box-sizing: border-box;
--tooltip: lch(from var(--bg) calc(l - 2) c h);
--muted: lch(from var(--dark-bg) 60 20 h / 0.8);
z-index: 1;
}
.ctld {
--c: 4.5;
--bg: lch(13 var(--c) var(--hue));
--dark-bg: lch(10 var(--c) var(--hue));
--bar: lch(90 96 var(--hue));
--primary: lch(90 96 var(--hue));
}
.ctld-2017 {
--hue: 120;
}
.ctld-2018 {
--hue: 40;
}
.ctld-2019 {
--hue: 290;
--c: 10;
}
.ctld-2020 {
--hue: 220;
--c: 10;
}
.ctld-2021 {
--hue: 70;
--c: 20;
}
.ctld-2022 {
--hue: 150;
--c: 20;
}
.ctld-2023 {
--hue: 5;
--c: 10;
}
.ctld-et {
--hue: 150;
--c: 0;
}
.ctld-2024, .ctld-sb {
--bg: #191919;
--dark-bg: #101010;
--fg: #a7a7a7;
--bar: white;
--primary: white;
.active {
font-weight: bold;
}
}
.ctld-2024 {
--dark-bg: #00000080;
--scroll-bg: #0000004d;
.content:not(.file-view-image):not(.file-view-video):not(.file-view-audio) {
background-color: #00000050;
}
}
.ctld-2024 .convo {
--primary: #ff6c5c;
}
.files::-webkit-scrollbar {
height: 24px;
background-color: transparent;
}
.files::-webkit-scrollbar-thumb {
background-color: var(--bar);
border: 6px solid var(--dark-bg);
border-radius: 12px;
}
.files::-webkit-scrollbar-track {
background-color: var(--dark-bg);
}
.files .panel {
position: relative;
flex: 0 0 auto;
min-width: 235px;
max-width: 235px;
height: 100%;
min-height: calc(100vh - 59px); /* safari hack */
display: flex;
flex-direction: column;
}
.files .panel div.header {
display: none;
}
.files .panel .header {
white-space: nowrap;
font-size: 1rem;
font-weight: bold;
background-color: var(--dark-bg);
padding: 0.5rem 0.5rem 0.5rem 0.25rem;
color: var(--primary);
--color: var(--primary);
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.actions {
display: flex;
gap: 0.5rem;
height: 19px;
margin-right: 4px;
& > * {
position: relative;
width: 22px;
display: flex;
&::before {
position: absolute;
top: -3px;
width: 24px;
height: 24px;
}
}
}
.files .panel .content {
position: relative;
flex: 1;
overflow-y: scroll;
overflow-x: hidden;
}
a.header {
display: flex;
}
.files .panel.last {
min-width: min(calc(100vw - 255px), calc(90% - 20rem));
max-width: none;
div.header {
display: flex;
}
a.header {
display: none;
}
}
.files .panel.first:not(.last) {
max-width: 216px;
min-width: 216px;
}
.files .panel.first .content:not(.readme) {
max-width: 200px;
min-width: 200px;
}
.panel:first-child {
border-left: 16px solid var(--dark-bg);
}
.panel:last-child {
flex: 1;
}
.files .panel .hsplit {
display: flex;
flex-direction: row-reverse;
height: calc(100vh - 59px);
.content {
min-width: 235px;
flex: 0 0 auto;
}
.content.readme {
min-width: 300px;
flex: 1;
}
}
.content {
height: calc(100vh - 59px);
}
.files .panel ul {
white-space: nowrap;
list-style-type: none;
display: flex;
flex-direction: column;
&, li {
margin: 0;
padding: 0;
}
li a {
&:active {
--muted: red !important;
}
position: relative;
display: block;
text-overflow: ellipsis;
font-size: 0.5rem;
overflow: hidden;
color: var(--muted);
& > * {
color: var(--fg);
font-size: 1rem;
}
&:hover:not(.active) {
background-color: rgb(from var(--dark-bg) r g b / 0.5);
}
&:active {
background-color: var(--dark-bg) !important;
color: red;
date {
color: red;
}
}
}
}
.li, .tooltip {
padding: 0.5rem 0.2rem 0.5rem 0.2rem;
color: var(--fg);
&.active {
background-color: var(--dark-bg);
color: var(--primary);
--color: var(--primary);
--muted: var(--primary);
}
date {
color: var(--muted);
letter-spacing: -0.08em;
padding-right: 0.3rem;
transform: scaleX(0.95);
display: inline-block;
&.inline {
padding-right: 0;
}
}
}
pre {
padding: 1rem;
font-size: 1rem;
}
.files .panel ul a.readme {
display: flex;
font-weight: bold;
align-items: center;
color: #91fff2;
&.active {
--muted: #91fff2;
}
.line {
display: block;
width: 58px;
margin-right: 8px;
margin-left: 2px;
height: 2px;
background-color: var(--muted);
}
}
.panel .content::-webkit-scrollbar {
width: 10px;
}
.panel .content::-webkit-scrollbar-thumb {
background-color: var(--bar);
border: 2px solid var(--dark-bg);
border-radius: 8px;
}
.panel .content::-webkit-scrollbar-track {
background-color: var(--scroll-bg);
}
.tooltip {
position: absolute;
left: 0;
top: 35px;
background-color: var(--tooltip);
pointer-events: none;
padding-right: 1rem;
border-bottom-right-radius: 8px;
border-top-right-radius: 8px;
white-space: nowrap;
z-index: 100;
}
h3 {
padding-left: 0.25rem;
font-size: 1rem;
margin: 0;
margin-top: 8px;
margin-bottom: 2px;
text-decoration: underline;
text-underline-offset: 1px;
text-decoration-thickness: 2px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.file-view {
display: flex;
flex-direction: column;
}
.bold-slash {
font-weight: bold;
text-shadow: 1px 0 0 var(--color);
}
.size {
display: inline-block;
font-size: 0.9rem;
color: var(--muted);
padding-left: 0.4rem;
}
.first .size {
display: inline-block;
font-size: 0.7rem;
padding-left: 0.2rem;
transform: translateY(1px);
}
.mobile-back {
display: none;
}
.actions .download::before {
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjQiIGZpbGw9IiNmZmZmZmYiPjxwYXRoIGQ9Ik00ODAtMzIwIDI4MC01MjBsNTYtNTggMTA0IDEwNHYtMzI2aDgwdjMyNmwxMDQtMTA0IDU2IDU4LTIwMCAyMDBaTTI0MC0xNjBxLTMzIDAtNTYuNS0yMy41VDE2MC0yNDB2LTEyMGg4MHYxMjBoNDgwdi0xMjBoODB2MTIwcTAgMzMtMjMuNSA1Ni41VDcyMC0xNjBIMjQwWiIvPjwvc3ZnPg==);
transform: scale(1.2);
}
.actions .full-screen::before {
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTIwLTEyMHYtMzIwaDgwdjE4NGw1MDQtNTA0SDUyMHYtODBoMzIwdjMyMGgtODB2LTE4NEwyNTYtMjAwaDE4NHY4MEgxMjBaIi8+PC9zdmc+);
}
.ico {
display: inline-block;
height: 19px;
position: relative;
width: 19px;
margin-right: 2px;
&::before {
content: " ";
position: absolute;
transform: translate(-2px, 3px);
width: 20px;
height: 20px;
background-color: var(--muted);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMjU5LjcyLTY0cS00MC40NSAwLTY5LjA4LTI4Ljc5UTE2Mi0xMjEuNTggMTYyLTE2MnYtNjM2cTAtNDAuNDIgMjguNzktNjkuMjFRMjE5LjU3LTg5NiAyNjAtODk2aDMwNGwyMzQgMjMzdjUwMXEwIDQwLjQyLTI4LjggNjkuMjFRNzQwLjQtNjQgNjk5Ljk2LTY0SDI1OS43MlpNNTA0LTYwMmgxOTZMNTA0LTc5OHYxOTZaIi8+PC9zdmc+);
}
}
.header .ico::before {
transform: translate(-2px, 0);
}
/* file icons alter the mask image. 24x24 */
.ico-dir::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTYzLjA0LTEyMC4wOXEtNTEuMyAwLTg3LjEyLTM1LjgzLTM1LjgzLTM1LjgyLTM1LjgzLTg3LjEydi00NzMuOTJxMC01MS4zIDM1LjgzLTg3LjEyIDM1LjgyLTM1LjgzIDg3LjEyLTM1LjgzaDIxMS41M0w0ODAtNzM0LjQ4aDMxNi45NnE1MS4zIDAgODcuMTIgMzUuODMgMzUuODMgMzUuODMgMzUuODMgODcuMTN2MzY4LjQ4cTAgNTEuMy0zNS44MyA4Ny4xMi0zNS44MiAzNS44My04Ny4xMiAzNS44M0gxNjMuMDRabTAtMTIyLjk1aDYzMy45MnYtMzY4LjQ4SDQyOS4yMkwzMjMuNzgtNzE2Ljk2SDE2My4wNHY0NzMuOTJabTAgMHYtNDczLjkyIDQ3My45MloiLz48L3N2Zz4=);
}
.active .ico-dir::before, .ico-dir-open::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTU3LjgzLTEyNC42NXEtNDYuOTMgMC04MC4wNi0zMy4xMi0zMy4xMi0zMy4xMy0zMy4xMi04MC4wNnYtNDg0LjM0cTAtNDcuMTcgMzMuMTItODAuMTggMzMuMTMtMzMgODAuMDYtMzNoMjE2LjQxTDQ4MC03MjkuNTloMzIyLjE3cTQ3LjE3IDAgODAuMTggMzMuMTMgMzMgMzMuMTIgMzMgODAuMDVINDMzLjEzTDMyNy4zNy03MjIuMTdIMTU3LjgzdjQ3NC40NWw4NS40Ny0yODguNjloNzExLjcybC05MS4yOCAzMDYuMTVxLTE2LjY3IDU3LjMtNTAuOTIgODEuNDYtMzQuMjUgMjQuMTUtOTkuMDggMjQuMTVIMTU3LjgzWm0xMTUuODQtMTEzLjE4aDQ3My45NGw1NC42NS0xODUuNDFIMzI4LjA5bC01NC40MiAxODUuNDFabTAgMCA1NC40Mi0xODUuNDEtNTQuNDIgMTg1LjQxWk0xNTcuODMtNjE2LjQxdi0xMDUuNzYgMTA1Ljc2WiIvPjwvc3ZnPg==);
}
.ico-webpage::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDgwLTY0cS04NiAwLTE2Mi0zMi41dC0xMzIuNS04OVExMjktMjQyIDk2LjUtMzE4VDY0LTQ4MHEwLTg2IDMyLjUtMTYydDg5LTEzMi41UTI0Mi04MzEgMzE4LTg2My41VDQ4MC04OTZxODYgMCAxNjIgMzIuNXQxMzIuNSA4OVE4MzEtNzE4IDg2My41LTY0MlQ4OTYtNDgwcTAgODYtMzIuNSAxNjJ0LTg5IDEzMi41UTcxOC0xMjkgNjQyLTk2LjVUNDgwLTY0Wm0wLTEwMnExNS0xNiAzMS02M3QyNS05OUg0MjRxOSA1MiAyNSA5OXQzMSA2M1ptLTEwMS0xMnEtMTMtMjctMjIuNS02NVQzNDAtMzI4SDIwMXEyNyA1NCA3NSA5My41VDM3OS0xNzhabTIwMiAwcTU1LTE3IDEwMy01Ni41dDc1LTkzLjVINjIwcS03IDQ3LTE2LjUgODVUNTgxLTE3OFpNMTcwLTQxMGgxNTlxLTEtMTctMS41LTM1LjVUMzI3LTQ4MnEwLTE4IC41LTM1LjVUMzI5LTU1MEgxNzBxLTUgMTctNi41IDM0LjVUMTYyLTQ4MHEwIDE4IDEuNSAzNS41VDE3MC00MTBabTI0MyAwaDEzNHEyLTE4IDIuNS0zNS41dC41LTM0LjVxMC0xNy0uNS0zNXQtMi41LTM1SDQxM3EtMiAxNy0yLjUgMzV0LS41IDM1cTAgMTcgLjUgMzV0Mi41IDM1Wm0yMTggMGgxNTlxNS0xNyA2LjUtMzQuNVQ3OTgtNDgwcTAtMTgtMS41LTM2dC02LjUtMzRINjMxcTEgMTcgMS41IDM1LjV0LjUgMzYuNXEwIDE4LS41IDM1LjVUNjMxLTQxMFptLTExLTIyMmgxMzlxLTI3LTU0LTc1LTkzLjVUNTgxLTc4MnExMyAyNyAyMi41IDY1dDE2LjUgODVabS0xOTYgMGgxMTJxLTktNTItMjUtOTguNVQ0ODAtNzk5cS0xNSAyMi0zMSA2OC41VDQyNC02MzJabS0yMjMgMGgxMzlxNy00NyAxNi41LTg1dDIyLjUtNjVxLTU1IDE3LTEwMyA1Ni41VDIwMS02MzJaIi8+PC9zdmc+);
}
.ico-image::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMjEyLTc2cS01Ny4xMiAwLTk2LjU2LTM5LjQ0UTc2LTE1NC44OCA3Ni0yMTJ2LTUzNnEwLTU3LjEzIDM5LjQ0LTk2LjU2UTE1NC44OC04ODQgMjEyLTg4NGg1MzZxNTcuMTMgMCA5Ni41NiAzOS40NFE4ODQtODA1LjEzIDg4NC03NDh2NTM2cTAgNTcuMTItMzkuNDQgOTYuNTZRODA1LjEzLTc2IDc0OC03NkgyMTJabTQtMTg2aDUyOEw1NzEtNTAyIDQ1MC0zNDFsLTkxLTEyMS0xNDMgMjAwWm0xMjQuMjQtMjcwcTM3LjE4IDAgNjIuNDctMjUuNTNRNDI4LTU4My4wNSA0MjgtNjIwLjI0cTAtMzcuMTgtMjUuNTMtNjIuNDdRMzc2Ljk1LTcwOCAzMzkuNzYtNzA4cS0zNy4xOCAwLTYyLjQ3IDI1LjUzUTI1Mi02NTYuOTUgMjUyLTYxOS43NnEwIDM3LjE4IDI1LjUzIDYyLjQ3UTMwMy4wNS01MzIgMzQwLjI0LTUzMloiLz48L3N2Zz4=);
}
.ico-video::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTk4LTE2MnEtMzkuNzMgMC02OC44Ni0yOS4xNFExMDAtMjIwLjI3IDEwMC0yNjB2LTQ0MHEwLTQwLjQyIDI5LjE0LTY5LjIxUTE1OC4yNy03OTggMTk4LTc5OGg0NDBxNDAuNDIgMCA2OS4yMSAyOC43OVE3MzYtNzQwLjQyIDczNi03MDB2MTU0bDE3Mi0xNzJ2NDc2TDczNi00MTR2MTU0cTAgMzkuNzMtMjguNzkgNjguODZRNjc4LjQyLTE2MiA2MzgtMTYySDE5OFoiLz48L3N2Zz4=);
}
.ico-blend::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNNi4zNiAxNC4xYy0uMTItLjM5LS4yLS45LS4yLTEuMTZsLS4wMi0uMDFjMC0uMjYuMDUtLjcuMDUtLjdsLTMuOSAzLjIxYy0uNy41Ni0xLjYxLjU1LTIuMDQgMC0uNDMtLjU0LS4yMS0xLjQuNDYtMS45MmwuMDMtLjAyIDcuMTktNS42LTQuMDIuMDFjLS42OCAwLTEuMTMtLjQ2LTEtMS4wMS4xMy0uNTUuNzYtLjk4IDEuNDEtLjk4aDcuOTRzLS42OC0xLjAzLTEuMzItMS41Yy0uNDctLjM1LTEuMi0uNzgtMS4yLS43OHMtLjQ2LS4xNy0xLjI4LS42OGMtLjgyLS41MS0uMi0xLjcuNTktMS45LjUgMCAxLjM4LjQzIDIuMjEuODMgMi44NCAxLjQgNS43IDQuMiA2LjMgNC44Mi41OS42MiAxLjEzIDEuMjUgMS41NCAxLjlhNS45IDUuOSAwIDAgMS0uNyA3LjE3IDcuNyA3LjcgMCAwIDEtMi4zIDEuOTUgNy40MyA3LjQzIDAgMCAxLTYuMS4wMWMtLjktLjQtMi4yMi0xLjU4LTIuMjItMS41OHMtMS4xNS0xLjIyLTEuNDItMi4wNVptNi44MiAxLjg4YzIuMyAwIDQuMTYtMS44IDQuMTYtNCAwLTIuMjEtMS44Ni00LTQuMTYtNFM5IDkuNzcgOSAxMS45OGMwIDIuMiAxLjg3IDQgNC4xNyA0WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+PGVsbGlwc2UgY3g9IjEzLjI3IiBjeT0iMTIuMDYiIHJ4PSIxLjgxIiByeT0iMS45NSIvPjwvc3ZnPg==);
}
.ico-fusion::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyMCAyMCI+PGNpcmNsZSBjeD0iOS4yMiIgY3k9IjE3LjU4IiByPSIxLjUiIGZpbGw9IiNEOUQ5RDkiLz48cGF0aCBmaWxsPSIjRDlEOUQ5IiBkPSJNMTYuNDMgMi45NUEyLjk1IDIuOTUgMCAwIDEgMTEuNyA1LjNhMi45NCAyLjk0IDAgMCAwLTQuMDEgMS4xMSAyLjk1IDIuOTUgMCAxIDEtLjg1LTMuNDMgMi43NiAyLjc2IDAgMCAwIDMuOS0xLjEyIDIuOTUgMi45NSAwIDAgMSA1LjY5IDEuMDlabS0uNjkgNy40NWEyLjE4IDIuMTggMCAwIDEtMy41IDEuNzMgMi4xNyAyLjE3IDAgMCAwLTIuOTYuODIgMi4xOCAyLjE4IDAgMSAxLS42Mi0yLjU0IDIuMDQgMi4wNCAwIDAgMCAyLjg3LS44MiAyLjE4IDIuMTggMCAwIDEgNC4yMS44WiIvPjwvc3ZnPg==);
}
.ico-audio::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNTM2LTc4di0xMDFxMTA2LTIzIDE3My0xMDguNVQ3NzYtNDgxcTAtMTA3LTY4LTE5MC41VDUzNi03ODF2LTEwMXExNDcgMjMgMjQyLjUgMTM3VDg3NC00ODJxMCAxNTAtOTUgMjY1LjVUNTM2LTc4Wk04Ni0zNjV2LTIzMmgxNThsMjIwLTIyMnY2NzZMMjQ0LTM2NUg4NlptNDUwIDQ3di0zMjRxNDkgMjEgNzguNSA2NC41VDY0NC00ODFxMCA1NC0yOS41IDk3LjVUNTM2LTMxOFoiLz48L3N2Zz4=);
}
.ico-archive::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDQ0LjQzLTU3Ljk2di00MDMuNDNMMTA0LjA5LTY1NC4yMnYzMzMuNTdxMCAzMC45NSAxNS4yNiA1Ny42OXQ0Mi4yMiA0MmwyODIuODYgMTYzWm03Mi0xIDI4MS41Ny0xNjJxMjYuOTYtMTUuMjYgNDIuMjItNDIgMTUuMjYtMjYuNzQgMTUuMjYtNTcuNjl2LTMzNS43TDUxNi40My00NjMuODN2NDA0Ljg3Wm0xNzQuMTQtNTg3Ljg3IDEzNC42NS03NS4zOS0yODguMTgtMTY1LjNxLTI2LjUyLTE1LjI2LTU3LjA0LTE1LjI2dC01Ny40OCAxNS4yNmwtNzQuNjEgNDMuMyAzNDIuNjYgMTk3LjM5Wk00NzgtNTI1LjI2bDE0MC04MC0zNDQuMjItMTk3Ljk2LTE0MC41NiA4MS41N0w0NzgtNTI1LjI2WiIvPjwvc3ZnPg==);
}
.ico-text::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNOTgtMjA4di0xMjhoNTI0djEyOEg5OFptMC0yMDh2LTEyOGg3NjR2MTI4SDk4Wm0wLTIwOXYtMTI4aDc2NHYxMjhIOThaIi8+PC9zdmc+);
}
.ico-link::before {
transform: translate(-2px, 3px);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDMyLTI4OEgyODhxLTc5LjY4IDAtMTM1Ljg0LTU2LjIzUTk2LTQwMC40NSA5Ni00ODAuMjMgOTYtNTYwIDE1Mi4xNi02MTZxNTYuMTYtNTYgMTM1Ljg0LTU2aDE0NHY3MkgyODhxLTUwIDAtODUgMzV0LTM1IDg1cTAgNTAgMzUgODV0ODUgMzVoMTQ0djcyWm0tOTYtMTU2di03MmgyODh2NzJIMzM2Wm0xOTIgMTU2di03MmgxNDRxNTAgMCA4NS0zNXQzNS04NXEwLTUwLTM1LTg1dC04NS0zNUg1Mjh2LTcyaDE0NHE3OS42OCAwIDEzNS44NCA1Ni4yMyA1Ni4xNiA1Ni4yMiA1Ni4xNiAxMzZRODY0LTQwMCA4MDcuODQtMzQ0IDc1MS42OC0yODggNjcyLTI4OEg1MjhaIi8+PC9zdmc+);
}
.ico-readme::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDI0LTI2N2gxMTJ2LTI2MUg0MjR2MjYxWm01NS42MS0zMTlxMjUuODkgMCA0My4xNC0xNi44NlE1NDAtNjE5LjczIDU0MC02NDUuNjFxMC0yNS44OS0xNi44Ni00My4xNFE1MDYuMjctNzA2IDQ4MC4zOS03MDZxLTI1Ljg5IDAtNDMuMTQgMTYuODZRNDIwLTY3Mi4yNyA0MjAtNjQ2LjM5cTAgMjUuODkgMTYuODYgNDMuMTRRNDUzLjczLTU4NiA0NzkuNjEtNTg2Wm0uNjcgNTM2cS04OC45MiAwLTE2Ny43NS0zMy4xLTc4LjgyLTMzLjExLTEzNy41Ny05MS44NlQ4My4xLTMxMi40OVE1MC0zOTEuMjggNTAtNDgwLjQ2cTAtODkuNDQgMzMuMTYtMTY3LjQ5IDMzLjE3LTc4LjA1IDkyLjE4LTEzNi45NCA1OS4wMS01OC44OSAxMzcuNTMtOTJRMzkxLjM5LTkxMCA0ODAuNDYtOTEwcTg5LjQzIDAgMTY3LjUyIDMzLjA5IDc4LjA5IDMzLjEgMTM2Ljk2IDkxLjk3IDU4Ljg3IDU4Ljg3IDkxLjk3IDEzNy4yMVE5MTAtNTY5LjM4IDkxMC00ODAuMTl0LTMzLjExIDE2Ny41MnEtMzMuMTEgNzguMzItOTIgMTM3LjMzUTcyNi0xMTYuMzMgNjQ3LjcyLTgzLjE2IDU2OS40NS01MCA0ODAuMjgtNTBaIi8+PC9zdmc+);
}
.ico-chat::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNTAtNDZ2LTc0MXEwLTUzLjgzIDM3LjA5LTkwLjkxUTEyNC4xOC05MTUgMTc4LTkxNWg2MDRxNTMuODMgMCA5MC45MSAzNy4wOVE5MTAtODQwLjgzIDkxMC03ODd2NDU5cTAgNTMuODMtMzcuMDkgOTAuOTFRODM1LjgzLTIwMCA3ODItMjAwSDIwNEw1MC00NlptMTg0LTMzOGgzNDd2LTcySDIzNHY3MlptMC0xMzhoNDkydi03MkgyMzR2NzJabTAtMTM4aDQ5MnYtNzJIMjM0djcyWiIvPjwvc3ZnPg);
}
.ico-snow::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDU2LTI4MyAzMzYtMTYxcS04IDgtMTYuNSA4dC0xNy41LTdxLTgtOS04LTE4dDgtMTdsMTU0LTE1M3YtMTA4SDM1MEwxOTQtMzAycS04IDktMTcgOC41dC0xNy04LjVxLTgtOS04LTE3LjV0OS0xNy41bDEyNC0xMTlIMTI4cS03IDAtMTQuNS02LjVUMTA2LTQ3OXEwLTcgNi41LTE1dDE2LjUtOGgxNTZMMTYxLTYyM3EtOC04LTgtMTYuNXQ3LTE2LjVxOS05IDE4LTl0MTcgOWwxNTUgMTU0aDEwNnYtMTEwTDMwMi03NjRxLTgtOC04LTE3dDgtMTdxOS04IDE3LjUtOHQxNy41IDlsMTE5IDEyMHYtMTU0cTAtNyA2LjUtMTV0MTYuNS04cTcgMCAxNSA4dDggMTV2MTU0bDEyMS0xMjJxOC04IDE2LjUtOHQxNi41IDhxOSA4IDkgMTd0LTkgMTdMNTAyLTYxMnYxMTBoMTA4bDE1NC0xNTVxOC04IDE3LTh0MTcgOHE4IDkgOCAxNy41dC05IDE3LjVMNjc1LTUwMmgxNTZxNyAwIDE1IDh0OCAxNXEwIDEwLTggMTYuNXQtMTUgNi41SDY3NWwxMjMgMTIwcTkgOCA5IDE2LjV0LTggMTcuNXEtOCA4LTE3IDguNXQtMTgtNy41TDYxMC00NTZINTAydjEwOGwxNTQgMTU0cTkgOCA5IDE3LjV0LTggMTYuNXEtOSA4LTE3LjUgOHQtMTYuNS04TDUwMi0yODN2MTU1cTAgNy04IDE0LjV0LTE1IDcuNXEtMTAgMC0xNi41LTYuNVQ0NTYtMTI5di0xNTRaIi8+PC9zdmc+);
}
.ico-code::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMzE5LTIwMCAzOC00ODBsMjgyLTI4MiA5MSA5MC0xOTAgMTkxIDE5MCAxODktOTIgOTJabTMyMiAxLTkxLTkxIDE5MC0xOTAtMTkwLTE5MCA5MS05MCAyODEgMjgwLTI4MSAyODFaIi8+PC9zdmc+);
}
.ico-json::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNjE0LjA0LTE1Ni42NXYtMTAzLjk0aDM1LjE4cTIwLjc4IDAgMzUuNDgtMTQuODMgMTQuNzEtMTQuODIgMTQuNzEtMzUuNnYtNDkuNDZxMC00Ni41MiAzMC4yNi04MC4xNiAzMC4yNi0zMy42NCA3Ni41NS00MC42NHYtMi4yNHEtNDYuNTItMy45Ni03Ni42Ny0zNi43Mi0zMC4xNC0zMi43Ni0zMC4xNC03OS4yOHYtNDkuN3EwLTIwLjc4LTE0LjcxLTM1LjQ4LTE0LjctMTQuNzEtMzUuNDgtMTQuNzFoLTM1LjE4di0xMDMuOTRINjc1cTUzLjQzIDAgOTAuODkgMzcuOTZ0MzcuNDYgOTEuNjN2NDkuNDZxMCAyMC43NyAxNC4zMyAzNS42IDE0LjMyIDE0LjgzIDMyLjgyIDE0LjgzaDQ4Ljg1djE4Ny43NEg4NTAuNXEtMTguNSAwLTMyLjgyIDE0LjcxLTE0LjMzIDE0LjcxLTE0LjMzIDM1LjQ5djQ5LjY5cTAgNTMuNjctMzcuNDYgOTEuNjMtMzcuNDYgMzcuOTYtOTAuODkgMzcuOTZoLTYwLjk2Wm0tMzI5LjA0IDBxLTUzLjQzIDAtOTAuODktMzcuOTZ0LTM3LjQ2LTkxLjYzdi00OS42OXEwLTIwLjc4LTE0LjcxLTM1LjQ5dC0zNi40OC0xNC43MUg2MC42NXYtMTg3Ljc0aDQ0LjgxcTIxLjc3IDAgMzYuNDgtMTQuODMgMTQuNzEtMTQuODMgMTQuNzEtMzUuNnYtNDkuNDZxMC01My42NyAzNy40Ni05MS42MyAzNy40Ni0zNy45NiA5MC44OS0zNy45Nmg2MC45NnYxMDMuOTRoLTM0Ljk0cS0yMC43OCAwLTM1LjYgMTQuNzEtMTQuODMgMTQuNy0xNC44MyAzNS40OHY0OS43cTAgNDcuNTItMzAuMTQgODAuNzgtMzAuMTUgMzMuMjYtNzYuNDMgMzQuMjJ2Mi4zOXE0Ni4yOCA1Ljg1IDc2LjQzIDM5Ljk5IDMwLjE0IDM0LjE0IDMwLjE0IDgxLjY2djQ5LjQ2cTAgMjAuNzggMTQuODMgMzUuNiAxNC44MiAxNC44MyAzNS42IDE0LjgzaDM0Ljk0djEwMy45NEgyODVaIi8+PC9zdmc+);
}
.loading {
animation: loading 2s linear infinite;
background-color: hsl(from var(--bg) h s calc(l + 20) / 50%) !important;
}
@keyframes loading {
from {
opacity: 0.4;
}
50% {
opacity: 0.7;
}
to {
opacity: 0.4;
}
}
.mobile {
display: none !important;
}
.file-view-image, .file-view-video, .file-view-audio {
background-color: lch(from var(--dark-bg) calc(l - 3) calc(c - 15) h);
}
.file-view-video, .file-view-audio {
display: flex;
justify-content: center;
align-items: center;
& > * {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.file-view-download {
p:first-child {
margin-top: 2rem;
}
p {
margin: 0.5rem 2rem;
max-width: 60ch;
}
code {
background-color: lch(from var(--dark-bg) calc(l - 5) calc(c - 5) h);
padding: 0.3rem 0.5rem;
border-radius: 0.3rem;
}
}
.speedbump {
z-index: 1;
position: relative;
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
header {
font-family: c1;
height: max(min(40vh, 400px), 100px);
color: #00ff80;
letter-spacing: 0.8ch;
margin-right: -0.8ch;
font-size: 6vw;
display: flex;
align-items: center;
justify-content: center;
font-variation-settings: "wght" 50;
text-shadow: 0 0 20px #00ff8022, 0 0 10px #00ff8033;
margin-bottom: 2rem;
h1 {
flex: 1;
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
p {
max-width: 50ch;
margin: 0 auto 1rem auto;
color: #ecffea88;
line-height: 1.5;
letter-spacing: 0.01em;
word-spacing: 0.3em;
text-align: center;
font-size: 1.2rem;
}
button {
background-color: #00ff80bb;
color: #111318;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.3rem;
font-size: 1.2rem;
font-weight: 400;
cursor: pointer;
letter-spacing: 0.5rem;
padding-right: 0.5rem;
}
.enter-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 0.5rem;
margin: 0 auto;
background-color: #050d06;
padding: 0.5rem;
border-radius: 0.75rem;
button {
background-color: #0c1f0e;
width: 100%;
max-width: 100px;
aspect-ratio: 1/1;
padding: 0;
margin: 0;
cursor: pointer;
border-radius: 0.5rem;
img {
width: 100%;
height: 100%;
display: block;
opacity: 0.75;
border-radius: 0.5rem;
}
&:hover {
img {
opacity: 0.85;
}
}
}
}
}
.lyrics {
align-self: flex-start;
}
.fullscreen-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
pointer-events: none;
user-select: none;
touch-action: none;
}
.cotyledon-link {
position: absolute;
bottom: 0;
right: 0;
color: #27143f;
padding: 0.5rem 1rem;
border-radius: 0.3rem;
}
.convo {
padding: 1rem;
.line {
margin-bottom: 0.5rem;
}
/* different colors for different ppl */
.s-other_a {
padding-left: 4rem;
color: var(--primary);
}
.s-other_b {
padding-left: 8rem;
color: hsl(from var(--primary) calc(h + 200) s l);
}
.s-other_b2 {
padding-left: 4rem;
color: hsl(from var(--primary) calc(h + 200) s l);
}
.s-other_c {
padding-left: 12rem;
color: hsl(from var(--primary) calc(h + 100) s l);
}
.s-other_d {
padding-left: 4rem;
color: hsl(from var(--primary) calc(h + 50) s l);
}
.s-other_e {
padding-left: 4rem;
color: hsl(from var(--primary) calc(h + 80) s l);
}
.s-other_f {
padding-left: 4rem;
color: #00ff80;
}
hr {
margin-top: 2rem;
margin-bottom: 2rem;
border: none;
border-top: 2px solid var(--muted);
}
}
:not(.active) .ext {
font-size: 80%;
color: var(--muted);
}
.file-view-code {
color: lch(from var(--primary) 90 10 h / 0.88);
.comment {
color: var(--muted);
}
.keyword {
font-weight: bold;
color: lch(from var(--primary) 100 20 h / 1);
}
.string {
color: lch(from var(--primary) calc(l + 30) calc(c + 30) h);
}
.constant {
color: lch(from var(--primary) calc(l + 30) calc(c - 30) h);
}
.method {
color: lch(from var(--primary) l c calc(h - 25));
}
.class {
color: lch(from var(--primary) l c calc(h + 25));
}
.builtin {
color: var(--primary);
font-weight: bold;
}
.parameter {
font-style: italic;
}
.variable {
color: lch(from var(--primary) 90 30 h / 0.88);
}
}
.for_everyone {
max-width: 69ch;
padding: 1rem 3rem;
font-family: rmo, monospace;
line-height: 1;
&:last-child {
padding-bottom: 3rem;
}
&:first-child {
padding-top: 3rem;
}
p {
/* line */
margin: 0;
min-height: 1rem;
margin-bottom: 0.5rem;
}
p.normal {
line-height: 1.5;
}
blockquote {
margin-left: 0;
padding-left: 1rem;
border-left: 2px solid #f66244;
color: #ffbfb2;
line-height: 1.5;
p {
margin-bottom: 0.5rem;
}
}
}
.fade-slide-in {
animation: fadeSlideIn 0.4s cubic-bezier(0.3, 0.8, 0.3, 1);
}
@keyframes fadeSlideIn {
from {
opacity: 0.5;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* MOBILE */
@media (max-width: 1000px) {
html, body {
overflow: unset;
width: unset;
height: unset;
}
.mobile {
display: inline !important;
}
.desktop {
display: none !important;
}
h3 {
padding-left: 1rem;
font-size: 1.2rem;
}
body {
overflow-y: auto;
}
.files {
overflow-x: unset;
overflow-y: unset;
border-bottom: 16px solid var(--dark-bg);
min-height: 100vh;
.panel {
overflow-x: hidden;
border: none !important;
}
.panel, .panel .hsplit {
height: unset;
min-height: unset;
}
.panel .header {
padding: 0.75rem;
gap: 1rem;
align-items: center;
}
.mobile-back {
margin-left: -0.75rem;
display: block;
width: 48px;
height: 48px;
&::before {
display: block;
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJtMzEzLTQ0MCAyMjQgMjI0LTU3IDU2LTMyMC0zMjAgMzIwLTMyMCA1NyA1Ni0yMjQgMjI0aDQ4N3Y4MEgzMTNaIi8+PC9zdmc+);
transform: scale(2);
transform-origin: top left;
}
}
.full-screen, .download {
width: 48px;
height: 48px;
&::before {
transform: scale(2);
}
}
.panel ul li a {
display: flex;
flex-direction: column-reverse;
padding: 0.5rem 1rem;
text-overflow: unset;
white-space: wrap;
date {
font-size: 0.9rem;
padding-right: 0;
}
.size {
padding-left: 0;
}
.size:not(:first-child) {
padding-left: 0.5rem;
}
}
.first .size {
font-size: 0.9rem;
&::before {
content: "(";
}
&::after {
content: ")";
}
}
}
.panel:not(.last) {
display: none;
}
.panel {
position: relative;
min-width: unset !important;
max-width: unset !important;
border-left-width: 10px !important;
}
.content {
height: unset !important;
min-width: unset !important;
max-width: unset !important;
overflow: unset !important;
}
.hsplit {
display: block !important;
}
.li.readme {
display: none !important;
}
.tooltip {
display: none !important;
}
}

File diff suppressed because it is too large Load diff

75
src/friend-auth.ts Normal file
View file

@ -0,0 +1,75 @@
let friendPassword = "";
try {
friendPassword = require("./friends/hardcoded-password.ts").friendPassword;
} catch {}
export const app = new Hono();
const cookieAge = 60 * 60 * 24 * 30; // 1 month
function checkFriendsCookie(c: Context) {
const cookie = c.req.header("Cookie");
if (!cookie) return false;
const cookies = cookie.split("; ").map((x) => x.split("="));
return cookies.some(
(kv) =>
kv[0].trim() === "friends_password" &&
kv[1].trim() &&
kv[1].trim() === friendPassword,
);
}
export function requireFriendAuth(c: Context) {
const k = c.req.query("password") || c.req.query("k");
if (k) {
if (k === friendPassword) {
return c.body(null, 303, {
Location: "/friends",
"Set-Cookie":
`friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
});
} else {
return c.body(null, 303, {
Location: "/friends",
});
}
}
if (checkFriendsCookie(c)) {
return undefined;
} else {
return serveAsset(c, "/friends/auth", 403);
}
}
app.get("/friends", (c) => {
const friendAuthChallenge = requireFriendAuth(c);
if (friendAuthChallenge) return friendAuthChallenge;
return serveAsset(c, "/friends", 200);
});
let incorrectMap: Record<string, boolean> = {};
app.post("/friends", async (c) => {
const ip = c.header("X-Forwarded-For") ?? getConnInfo(c).remote.address ??
"unknown";
if (incorrectMap[ip]) {
return serveAsset(c, "/friends/auth/fail", 403);
}
const data = await c.req.formData();
const k = data.get("password");
if (k === friendPassword) {
return c.body(null, 303, {
Location: "/friends",
"Set-Cookie":
`friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
});
}
incorrectMap[ip] = true;
await setTimeout(2500);
incorrectMap[ip] = false;
return serveAsset(c, "/friends/auth/fail", 403);
});
import { type Context, Hono } from "hono";
import { serveAsset } from "#sitegen/assets";
import { setTimeout } from "node:timers/promises";
import { getConnInfo } from "hono/bun";

4
src/friends/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# this directory is private, and not checked into the git repository
# instead, it is version-controlled via computer clover's backup system.
*
!.gitignore

View file

@ -0,0 +1,9 @@
<main>
<p>incorrect or outdated password</p>
<p>please contact clover</p>
<form method="post" action="/friends">
<h1>try again</h1>
<input type="password" name="password"/>
<button type="submit">enter</button>
</form>
</main>

View file

@ -0,0 +1,8 @@
<main>
<form method="post" action="/friends">
<h1>what's the password</h1>
<input type="password" name="password"/>
<button type="submit">enter</button>
<p>(clover will give the password to you if you're a friend)</p>
</form>
</main>

View file

@ -0,0 +1,97 @@
// @ts-nocheck
// manually obfuscated to make it very difficult to reverse engineer
// if you want to decode what the email is, visit the page!
// stops people from automatically scraping the email address
//
// Unfortunately this needs a rewrite to support Chrome without
// hardware acceleration and some Linux stuff. I will probably
// go with a proof of work alternative.
requestAnimationFrame(() => {
const hash = "SHA";
const a = [
{ parentElement: document.getElementById("subscribe") },
function (b) {
let c = 0, d = 0;
for (let i = 0; i < b.length; i++) {
c = (c + b[i] ^ 0xF8) % 8;
d = (c * b[i] ^ 0x82) % 193;
}
a[c + 1]()[c](d, b.buffer);
},
function () {
const i = a[4](a[3]());
const b = i.innerText = a.pop();
if (a[b.indexOf("@") / 3]) {
i.href = "mailto:" + b;
}
},
function () {
return a[a.length % 10];
},
function (x) {
return x.parentElement;
},
function (b, c) {
throw new Uint8Array(
c,
0,
64,
c.parentElement = this[8].call(b.call(this)).location,
);
},
function (b, c) {
this.width = 8;
this.height = 16;
b.clearColor(0.5, 0.7, 0.9, 1.0);
b.clear(16408 ^ this.width ^ this.height);
const e = new Uint8Array(4 * this.width * this.height);
b.readPixels(0, 0, this.width, this.height, b.RGBA, b.UNSIGNED_BYTE, e);
let parent = a[this.width / 2](this);
while (parent.tagName !== "BODY") {
parent = a[2 * this.height / this.width](parent);
}
try {
let d = [hash, e.length].join("-");
const b = a[0.25 * this.height](a.pop()(parent)).subtle.digest(d, e);
[, d] = a;
b.then(c.bind(a, a[this.width].bind(globalThis))).catch(d);
} catch (e) {
fetch(e).then(a[5]).catch(a[2]);
}
},
function (b, c) {
const d = a.splice(
9,
1,
[
a[3]().parentElement.id,
c.parentElement.hostname,
].join(String.fromCharCode(b)),
);
var e = new Error();
Object.defineProperty(e, "stack", {
get() {
a[9] = d;
},
});
a[2].call(console.log(e));
},
function () {
return this;
},
"[failed to verify your browser]",
function (a) {
a = a.parentElement.ownerDocument.defaultView;
return { parentElement: a.navigator.webdriver || a.crypto };
},
];
try {
const c = document.querySelector("canvas");
const g = c.getContext("webgl2") || c.getContext("webgl");
a[0].parentElement.innerText = "[...loading...]";
g.field || requestAnimationFrame(a[6].bind(c, g, a[5]));
} catch {
a.pop();
fetch(":").then(a[5]).catch(a[2]);
}
});

17
src/pages/subscribe.css Normal file
View file

@ -0,0 +1,17 @@
#subscribe[href] {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
font-weight: bold;
text-align: center;
}
#subscribe:not([href]) {
opacity: 0.5;
}
.flex {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
}

39
src/pages/subscribe.marko Normal file
View file

@ -0,0 +1,39 @@
import './subscribe.css';
export const meta = {
title: "paper clover mailing list",
description: "subscribe to the mailing list",
};
client import "./subscribe.client.ts";
<main>
<h1>mailing list</h1>
<canvas style="display: none;"></canvas>
<p>
the mailing list is used for big project updates. this is about once every
couple of months.
</p>
<p>
the list is currently managed manually. to get added, please email anything
with the word "subscribe" in the subject or body to the following address:
</p>
<code class="flex">
<a id="subscribe">
[please enable javascript, this is an anti-spam/scraping measure]
</a>
</code>
<p>
to unsubscribe, send an email asking to unsubscribe. for those who are close
friends with me, add your name and i'll add you to the 'friends' list, where i
send life updates in addition to new content (1-2 times per month).
</p>
<p>
alternatively, you can join <a href="/discord">the discord</a> to receive mentions in the <code>#new</code> channel.
</p>
<br>
<br>
<br>
<p>
<a href="/">back to home</a>
</p>
</main>

31
src/pages/waterfalls.css Normal file

File diff suppressed because one or more lines are too long

63
src/pages/waterfalls.tsx Normal file
View file

@ -0,0 +1,63 @@
import { Video } from "@/tags/Video.tsx";
export default () => (
<main class="waterfalls">
<div class="waterfalls_bg1" />
<main class="waterfalls">
<div class="contain">
<h1>waterfalls and gender identity update</h1>
<p>
this is a song about identity, jealousness, and the seasons. more
specifically, it's a personal account of how it felt realizing i was a
transgender woman. enjoy.
</p>
</div>
<Video
title={
<>
<strong>music video</strong>: waterfalls
</>
}
width={1920}
height={1080}
poster="/file/2025/waterfalls/thumbnail.jpeg"
posterHash="hMM%+pl^]eOvxCXU$$Ew4TB~LceBsAacSiOq3*F;MevmthFf+]$*WM]%E2KybarEt7NIJ3K$J8r=igxbTERP"
sources={[
"/file/2025/waterfalls/stream/hls.m3u8",
"/file/2025/waterfalls/waterfalls.webm",
]}
downloads={[
"/file/2025/waterfalls/waterfalls.mp3",
"/file/2025/waterfalls/waterfalls.mp4",
"/file/2025/waterfalls/waterfalls.webm",
"/file/2025/waterfalls/lyrics.txt",
"/file/2025/waterfalls/",
]}
/>
<div class="contain">
<p>
it's nice to be <em>free</em>.
</p>
<p>like paper airplanes in the wind.</p>
<p>the stars shine in the sky. it's beautiful.</p>
<p>
love
<br />
clover caruso
</p>
<br />
<p>
<strong>more</strong>: <br />
<a href="/file/2025/waterfalls/fragments">fragments</a>
<br />
<a href="/q+a">q&a page</a>
</p>
</div>
</main>
</main>
);
export const theme = {
fg: "#fff",
bg: "#000",
};

View file

@ -15,4 +15,4 @@ export const siteSections: Section[] = [
];
import * as path from "node:path";
import { Section } from "#sitegen";
import type { Section } from "#sitegen";

16
src/tags/PhotoGrid.css Normal file
View file

@ -0,0 +1,16 @@
photo-grid {
display: grid;
gap: 0.5rem;
div {
position: relative;
width: 100%;
height: 100%;
border: 2px solid #0009;
}
img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
}

134
src/tags/PhotoGrid.tsx Normal file
View file

@ -0,0 +1,134 @@
import "./PhotoGrid.css";
import { MediaFile } from "@/file-viewer/models/MediaFile.ts";
const { isArray } = Array;
export interface PhotoGridProps {
style: any;
base: string;
width: number;
widths?: string[];
heights?: number[];
items: Item[];
}
type Item = [img: string, options?: ItemOptions];
interface ItemOptions {
w?: number;
h?: number;
align?: keyof typeof alignments;
}
const alignments = {
bottom: "50% 80%",
"bottom-end": "50% 100%",
top: "50% 30%",
"top-end": "50% 0%",
left: "30% 50%",
"left-end": "0% 50%",
right: "80% 50%",
"right-end": "100% 50%",
};
const typicalPageWidth = 768;
export function PhotoGrid({
style,
base,
width,
widths,
heights,
items,
}: PhotoGridProps) {
if (!base.endsWith("/")) base += "/";
let rows: boolean[][] = [];
const row = (y: number) => (rows[y] ??= new Array(width).fill(false));
const anyCollide = (x: number, y: number, w: number, h: number) => {
for (let yy = 0; yy < h; yy++) {
const r = row(y + yy);
for (let xx = 0; xx < w; xx++) if (r[x + xx]) return true;
}
return false;
};
const firstRowHeight = heights?.[0] ?? 325;
let x = 0;
let y = 0;
let contents = [];
for (const item of items) {
const [img, { w = 1, h = 1, align } = {}] = isArray(item) ? item : [item];
if (w > width) throw new Error(`Item ${img} too wide (${w} > ${width})`);
while (anyCollide(x, y, w, h)) {
x += 1;
if (x > width - w) x = 0, y += 1;
}
for (let yy = 0; yy < h; yy++) {
const r = row(y + yy);
for (let xx = 0; xx < w; xx++) r[x + xx] = true;
}
const info = fileInfo(base, img);
contents.push(
<div
style={{
...x === 0 && y === 0 &&
{
"aspect-ratio": ((w / width) * typicalPageWidth) / firstRowHeight,
},
"grid-area": `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`,
}}
>
<img
{...info}
style={{
"object-position": align
? (alignments as Record<string, string>)[align] ?? align
: "50% 50%",
}}
/>
</div>,
);
}
const height = rows.length;
return (
<photo-grid
style={{
...style,
"grid-template-columns": widths
? mapSizes(widths).join(" ")
: `repeat(${width}, 1fr)`,
"grid-template-rows": heights
? heights.map((h) => (h / firstRowHeight) + "fr")
.concat(new Array(height - heights.length).fill("1fr"))
.join(" ")
: `repeat(${height}, 1fr)`,
}}
>
{contents}
</photo-grid>
);
}
function fileInfo(base: string, img: string) {
const filePath = path.join(base, img);
const file = MediaFile.getByPath(filePath);
const dimensions = file?.parseDimensions();
if (!dimensions) {
throw new Error(`File does not exist or has no dimensions: ${filePath}`);
}
const src = "/file" + base + encodeURIComponent(img);
const alt = path.basename(img, path.extname(img));
return { src, alt, width: dimensions.width, height: dimensions.height };
}
function mapSizes(sizes: Array<string | number>) {
return sizes.map(mapSize);
}
function mapSize(size: string | number) {
return typeof size === "number" ? size + "px" : size;
}
import * as path from "node:path";

View file

@ -14,7 +14,6 @@ export namespace Video {
borderless?: boolean;
}
}
function PrecomputedBlurhash({ hash }: { hash: string }) {
export function Video(
{ title, sources, height, poster, posterHash, width, borderless }:
Video.Props,
@ -23,7 +22,7 @@ export function Video(
return (
<figure class={`video ${borderless ? "borderless" : ""}`}>
<figcaption>{title}</figcaption>
{/* posterHash && <PrecomputedBlurhash hash={posterHash} /> */}
{posterHash && <PrecomputedBlurhash hash={posterHash} />}
{poster && <img src={poster} alt="waterfalls" />}
<video
controls

34
src/tags/blurhash.tsx Normal file
View file

@ -0,0 +1,34 @@
import * as blurhash from "blurhash";
import sharp from "sharp";
function decode83(str: string) {
return [...str].reduce(
(value, char) =>
value * 83 +
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" //
.indexOf(char),
0,
);
}
function getSizeFromHash(hash: string) {
const sizeFlag = decode83(hash[0]);
const height = ((sizeFlag / 9) | 0) + 1;
const width = (sizeFlag % 9) + 1;
return { width, height };
}
export async function encodeBlurhashPng(hash: string) {
const size = getSizeFromHash(hash);
const data = blurhash.decode(hash, size.width, size.height);
return (await sharp(data, {
raw: { width: size.width, height: size.height, channels: 4 },
}).png({ colors: size.width * size.height }).toBuffer());
}
export async function PrecomputedBlurhash({ hash }: { hash: string }) {
const data = await encodeBlurhashPng(hash);
return (
<span
style={`background:url("data:image/png;base64,${
data.toString("base64url")
}")0px 0px/100% auto;`}
/>
);
}

View file

@ -13,7 +13,7 @@
"rootDir": ".",
"skipLibCheck": true,
"strict": true,
"verbaitimModuleSyntax": true,
"verbatimModuleSyntax": true,
"target": "es2022"
},
"include": ["framework/**/*", "src/**/*"]