the home site for me: also iteration 3 or 4 of my site

Compare changes

Choose any two refs to compare.

Changed files
+11558 -1831
.github
content
highlight_themes
hooks
sass
scripts
static
badges
blog
adding-a-copy-button
airbuds
analyzing-implications-of-online-safety-legislation
atuin
degraded-zpool-proxmox
exporting-from-plausible
garmin-vivoactive-homeassistant
hilton-tomfoolery
install-truenas-core-proxmox
mega
monaspace-vs-code-install
my-animations
my-life-story-with-tech
remove-exif-git-hook
spherical-ray-diagrams
spotify-to-apple-music
ssd-removal-mbp-2017
tangled-sync
test-post
favicon
js
pfp
pfps
tags
accessibility
apple
archival
atproto
biography
cool-stuff
essays
fancy
graphql
hilton
homelab
meta
mildrant
music
nix
physics
project
reverse-engineering
shell
teardown
tool
tutorial
yap-fest
verify
syntaxes
templates
tools
.github/images/og.png

This is a binary file and will not be displayed.

.github/images/preview.webp

This is a binary file and will not be displayed.

.github/images/ss.png

This is a binary file and will not be displayed.

+4 -1
.gitignore
··· 1 public 2 node_modules 3 - .env
··· 1 public 2 + .zola-build 3 node_modules 4 + .env 5 + .crush 6 + .DS_Store
+5
.imgbotconfig
···
··· 1 + { 2 + "schedule": "daily", 3 + "aggressiveCompression": "true" 4 + } 5 +
+45 -12
README.md
··· 1 - # site@zera 2 3 - <figure align="center"> 4 - <img src="https://github.com/kcoderhtml/zera/blob/master/.github/images/ss.png?raw=true" alt="screenshot of the website"/> 5 - <figcaption><i>My site v4 (i think)</i></figcaption> 6 - </figure> 7 8 ## Special Features 9 10 - The whole website can be statically rendered in `~93ms` 11 - Deployed via cloudflare pages with a total push to deploy time of `~20s` 12 - - self hosted analytics with plausible, publicaly accessible at this [dashboard](https://nexus.kieranklukas.com/dunkirk.sh/) 13 - Automatic OG image via a custom script using puppeteer. 14 - ![og image example](https://github.com/kcoderhtml/zera/blob/master/.github/images/og.png?raw=true) 15 16 ## Awesome projects that made this possible 17 18 Huge thanks to [Speyll/anemone](https://github.com/Speyll/anemone) for the template that helped me understand [Zola](https://www.getzola.org/) 19 20 - This site's theme is based off of the awesome project [Speyll/suCSS/](https://github.com/) with my own flavoring on top and the code theme is based off of [uncomfyhalomacro/catppuccin-zola](https://github.com/uncomfyhalomacro/catppuccin-zola) modified to work with `data-theme`. 21 22 - --- 23 24 - _ยฉ 2024 Kieran Klukas_ 25 - _Content Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)_ 26 - _Code Licensed under [AGPL 3.0](LICENSE.md)_
··· 1 + <h3 align="center"> 2 + <img src="https://cloud-4mfbnf9u2-hack-club-bot.vercel.app/0img_3132.png" width="350" alt="site@zera"/> 3 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/transparent.png" height="30" width="0px"/> 4 + </h3> 5 + 6 + <p align="center"> 7 + <i>My site v4 (i think)</i> 8 + </p> 9 + 10 + <p align="center"> 11 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break-thin.svg" /> 12 + </p> 13 + 14 + ![screenshot of the website](https://raw.githubusercontent.com/taciturnaxolotl/zera/refs/heads/main/.github/images/preview.webp) 15 + 16 17 + <p align="center"> 18 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break-thin.svg" /> 19 + </p> 20 21 ## Special Features 22 23 - The whole website can be statically rendered in `~93ms` 24 - Deployed via cloudflare pages with a total push to deploy time of `~20s` 25 + - blazing fast privacy preserving view counter with [abacus](https://jasoncameron.dev/abacus/) 26 + ```html 27 + <script> 28 + function cb(res) { 29 + const fmt = new Intl.NumberFormat('en', { notation: 'compact' }); 30 + const elements = document.querySelectorAll("[id='visits']"); 31 + elements.forEach(el => { 32 + el.innerText = fmt.format(res.value); 33 + el.title = res.value + " visits"; 34 + }); 35 + } 36 + </script> 37 + <script async src="https://abacus.jasoncameron.dev/hit/namespace/counter?callback=cb"></script> 38 + ``` 39 - Automatic OG image via a custom script using puppeteer. 40 + ![og image example](https://raw.githubusercontent.com/taciturnaxolotl/zera/refs/heads/main/static/blog/hilton-tomfoolery/og.png) 41 42 ## Awesome projects that made this possible 43 44 Huge thanks to [Speyll/anemone](https://github.com/Speyll/anemone) for the template that helped me understand [Zola](https://www.getzola.org/) 45 46 + This site's theme is based off of the awesome project [Speyll/suCSS/](https://github.com/) with my own flavoring on top and the code theme is based off of [uncomfyhalomacro/catppuccin-zola](https://github.com/uncomfyhalomacro/catppuccin-zola) modified to work with `data-theme` (and then removed again lol). 47 48 + <p align="center"> 49 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" /> 50 + </p> 51 52 + <p align="center"> 53 + <code>&copy 2024-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a></code> 54 + </p> 55 + 56 + <p align="center"> 57 + <a href="https://github.com/taciturnaxolotl/zera/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=Code License&message=AGPL 3.0&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 58 + <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=Content License&message=CC BY-NC-SA 4.0&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 59 + </p>
+25 -29
biome.json
··· 1 { 2 - "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 - "vcs": { 4 - "enabled": false, 5 - "clientKind": "git", 6 - "useIgnoreFile": false 7 - }, 8 - "files": { 9 - "ignoreUnknown": false, 10 - "ignore": [] 11 - }, 12 - "formatter": { 13 - "formatWithErrors": true, 14 - "enabled": true, 15 - "indentStyle": "tab" 16 - }, 17 - "organizeImports": { 18 - "enabled": true 19 - }, 20 - "linter": { 21 - "enabled": true, 22 - "rules": { 23 - "recommended": true 24 - } 25 - }, 26 - "javascript": { 27 - "formatter": { 28 - "quoteStyle": "double" 29 - } 30 - } 31 }
··· 1 { 2 + "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", 3 + "vcs": { 4 + "enabled": false, 5 + "clientKind": "git", 6 + "useIgnoreFile": false 7 + }, 8 + "files": { 9 + "ignoreUnknown": false 10 + }, 11 + "formatter": { 12 + "formatWithErrors": true, 13 + "enabled": true, 14 + "indentStyle": "tab" 15 + }, 16 + "linter": { 17 + "enabled": true, 18 + "rules": { 19 + "recommended": true 20 + } 21 + }, 22 + "javascript": { 23 + "formatter": { 24 + "quoteStyle": "double" 25 + } 26 + } 27 }
+304
bun.lock
···
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "dependencies": { 7 + "@types/cli-progress": "^3.11.6", 8 + "cli-progress": "^3.12.0", 9 + "dotenv": "^16.4.7", 10 + "glob": "^13.0.0", 11 + "sharp": "^0.34.5", 12 + }, 13 + "devDependencies": { 14 + "@types/bun": "latest", 15 + "puppeteer": "^23.6.0", 16 + }, 17 + }, 18 + }, 19 + "packages": { 20 + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], 21 + 22 + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 23 + 24 + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], 25 + 26 + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], 27 + 28 + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], 29 + 30 + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], 31 + 32 + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], 33 + 34 + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], 35 + 36 + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], 37 + 38 + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], 39 + 40 + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], 41 + 42 + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], 43 + 44 + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], 45 + 46 + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], 47 + 48 + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], 49 + 50 + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], 51 + 52 + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], 53 + 54 + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], 55 + 56 + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], 57 + 58 + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], 59 + 60 + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], 61 + 62 + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], 63 + 64 + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], 65 + 66 + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], 67 + 68 + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], 69 + 70 + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], 71 + 72 + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], 73 + 74 + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], 75 + 76 + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], 77 + 78 + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], 79 + 80 + "@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="], 81 + 82 + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], 83 + 84 + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 85 + 86 + "@types/cli-progress": ["@types/cli-progress@3.11.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA=="], 87 + 88 + "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], 89 + 90 + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], 91 + 92 + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], 93 + 94 + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 95 + 96 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 97 + 98 + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 99 + 100 + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], 101 + 102 + "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], 103 + 104 + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], 105 + 106 + "bare-fs": ["bare-fs@4.5.2", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw=="], 107 + 108 + "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="], 109 + 110 + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], 111 + 112 + "bare-stream": ["bare-stream@2.7.0", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A=="], 113 + 114 + "bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="], 115 + 116 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 117 + 118 + "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], 119 + 120 + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], 121 + 122 + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], 123 + 124 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 125 + 126 + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 127 + 128 + "chromium-bidi": ["chromium-bidi@0.11.0", "", { "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA=="], 129 + 130 + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], 131 + 132 + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], 133 + 134 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 135 + 136 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 137 + 138 + "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], 139 + 140 + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], 141 + 142 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 143 + 144 + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], 145 + 146 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 147 + 148 + "devtools-protocol": ["devtools-protocol@0.0.1367902", "", {}, "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="], 149 + 150 + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], 151 + 152 + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 153 + 154 + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], 155 + 156 + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], 157 + 158 + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], 159 + 160 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 161 + 162 + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], 163 + 164 + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], 165 + 166 + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 167 + 168 + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 169 + 170 + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], 171 + 172 + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], 173 + 174 + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], 175 + 176 + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], 177 + 178 + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 179 + 180 + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], 181 + 182 + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], 183 + 184 + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], 185 + 186 + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], 187 + 188 + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], 189 + 190 + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 191 + 192 + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 193 + 194 + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], 195 + 196 + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], 197 + 198 + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 199 + 200 + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 201 + 202 + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], 203 + 204 + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], 205 + 206 + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 207 + 208 + "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], 209 + 210 + "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], 211 + 212 + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], 213 + 214 + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], 215 + 216 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 217 + 218 + "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], 219 + 220 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 221 + 222 + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], 223 + 224 + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], 225 + 226 + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 227 + 228 + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], 229 + 230 + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], 231 + 232 + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], 233 + 234 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 235 + 236 + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], 237 + 238 + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], 239 + 240 + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], 241 + 242 + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], 243 + 244 + "puppeteer": ["puppeteer@23.11.1", "", { "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1367902", "puppeteer-core": "23.11.1", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw=="], 245 + 246 + "puppeteer-core": ["puppeteer-core@23.11.1", "", { "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "debug": "^4.4.0", "devtools-protocol": "0.0.1367902", "typed-query-selector": "^2.12.0", "ws": "^8.18.0" } }, "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg=="], 247 + 248 + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], 249 + 250 + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 251 + 252 + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 253 + 254 + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], 255 + 256 + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], 257 + 258 + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], 259 + 260 + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], 261 + 262 + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 263 + 264 + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], 265 + 266 + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 267 + 268 + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 269 + 270 + "tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], 271 + 272 + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], 273 + 274 + "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], 275 + 276 + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], 277 + 278 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 279 + 280 + "typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="], 281 + 282 + "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], 283 + 284 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 285 + 286 + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 287 + 288 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 289 + 290 + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 291 + 292 + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 293 + 294 + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], 295 + 296 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 297 + 298 + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], 299 + 300 + "zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], 301 + 302 + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], 303 + } 304 + }
bun.lockb

This is a binary file and will not be displayed.

+7 -9
config.toml
··· 3 4 title = "site@zera" 5 description = "The home site of kieran klukas" 6 compile_sass = true 7 minify_html = true 8 generate_feeds = true 9 default_language = "en" 10 11 - taxonomies = [ 12 - {name = "tags", feed = true}, 13 - ] 14 15 [markdown] 16 render_emoji = true ··· 19 20 highlight_code = true 21 highlight_theme = "css" 22 23 [slugify] 24 paths = "on" ··· 32 author = "Kieran Klukas" 33 display_author = true 34 35 - favicon = "favicon.ico" 36 - image = "" 37 - 38 default_theme = "light" 39 list_pages = false 40 twitter_card = true 41 42 header_nav = [ 43 - { url = "/", name = "/home/" }, 44 - { url = "/pfp", name = "/pfp/" }, 45 - { url = "/blog", name = "/blog/" } 46 ]
··· 3 4 title = "site@zera" 5 description = "The home site of kieran klukas" 6 + author = "kieran@dunkirk.sh (Kieran Klukas)" 7 compile_sass = true 8 minify_html = true 9 generate_feeds = true 10 + feed_filenames = ["rss.xml", "atom.xml"] 11 default_language = "en" 12 13 + taxonomies = [{ name = "tags", feed = true }] 14 15 [markdown] 16 render_emoji = true ··· 19 20 highlight_code = true 21 highlight_theme = "css" 22 + extra_syntaxes_and_themes = ["syntaxes"] 23 24 [slugify] 25 paths = "on" ··· 33 author = "Kieran Klukas" 34 display_author = true 35 36 default_theme = "light" 37 list_pages = false 38 twitter_card = true 39 40 header_nav = [ 41 + { url = "/", name = "/root" }, 42 + { url = "/verify", name = "/verify" }, 43 + { url = "/blog", name = "/blog" }, 44 ]
+29 -22
content/_index.md
··· 1 +++ 2 +++ 3 4 - <div style="display: flex; justify-content: center;"> 5 - <img src="/pfps/current.webp" alt="an image of kieran holding a white kitten" width="512" height="512"/> 6 </div> 7 8 - ## About me 9 10 - Erlo! My name is Kieran Klukas and i'm a homeschooled coder who is {{ age(length=0) }} years old and loves film making, fpv, and typescript :) 11 12 - > init.ts 13 - ```ts 14 - const kieran = { 15 - name: "kieran klukas" 16 - age: {{ age(length=2) }} 17 - education: ["Homeschooled", "Dual Enrollment"] 18 - favFoods: ["lo mein", "bacon fried rice", "pretty much any meat"] 19 } 20 ``` 21 22 - A few special features this site has 23 - - The whole website can be statically rendered in `~93ms` 24 - - Deployed via cloudflare pages with a total push to deploy time of `~20s` 25 - - Automatic OG image via a custom script using puppeteer. 26 - - self hosted analytics with plausible, publicaly accessible at this [dashboard](https://nexus.kieranklukas.com/dunkirk.sh/) 27 28 - ## Want to talk to me? 29 30 - Do you want to hire me? (I will answer immediately :) If you just have a question or want to talk I'll still answer (admittedly more sadly). 31 32 - - Email: [me@dunkirk.sh](mailto:me@dunkirk.sh) 33 - - Matrix: [@kieran:dumpsterfire.icu](https://matrix.to/#/@kieran.matrix.dumpsterfire.icu) 34 - Hackclub Slack: [@krn](https://hackclub.slack.com/team/U062UG485EE) (only if you are a highschooler or younger; [join here](https://hackclub.com/slack/)) 35 - 36 - _I wouldn't count on reaching me via Matrix. I tend to check it once in a blue moon; email is probably your best bet._
··· 1 +++ 2 +++ 3 4 + <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 2rem;"> 5 + <img src="/pfps/fall.jpg" alt="kieran wearing a robotics sweatshirt and standing in front of a tree with fall leaves" width="512" height="512" class="u-photo"/> 6 + {{ is() }} 7 </div> 8 9 + # About me 10 + 11 + Erlo! My name is Kieran Klukas i'm {{ age(length=0) }} years old and love cyber, anything with micro-controllers, obscure languages, nix :nix:, and yummy food :) 12 13 + > flake.nix 14 15 + ```nix 16 + { 17 + description = "a short bit about me"; 18 + 19 + outputs = { self, ... }: 20 + let 21 + kieran = rec { 22 + name = "Kieran Klukas"; 23 + pronouns = "he/him"; 24 + aliases = [ "taciturnaxolotl" "krn" ]; 25 + location = "Westerville, Ohio, USA"; 26 + hobbies = [ "frc" "ctfs" "random side projects"]; 27 + }; 28 + in 29 + { 30 + inherit kieran; 31 + }; 32 } 33 ``` 34 35 + this site has page hits (<code id="visits">0</code> and counting) via [abacus](https://jasoncameron.dev/abacus/) but they are completely anonymous and just http requests so no sketchy analytics here! 36 37 + # Want to talk to me? 38 39 + I'm open to projects or just random questions! Feel free to reach out with any of the following or anything on [/verify](/verify) 40 41 + - Email: [kieran@dunkirk.sh](mailto:kieran@dunkirk.sh) 42 - Hackclub Slack: [@krn](https://hackclub.slack.com/team/U062UG485EE) (only if you are a highschooler or younger; [join here](https://hackclub.com/slack/)) 43 + - If you just want to know when I make a new post then you can subscribe to the [:rss:](rss.xml) feed
+22 -21
content/blog/2023-07-10_install-truenas-core-proxmox.md
··· 6 7 [taxonomies] 8 tags = ["tutorial", "archival"] 9 - 10 - [extra] 11 - has_toc = true 12 +++ 13 14 - {{ img(id="https://cloud-f19fn8u8j-hack-club-bot.vercel.app/0image.png" alt="screenshot of the vault vm in proxmox" caption="my active vault storing 1.8TB of old projects") }} 15 16 ## Introduction 17 ··· 27 28 Sign-in to Proxmox and upload your ISO to the local storage or, download the file directly from the link using the built-in ISO fetcher. 29 30 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Ww212rUDQ_Ms9P2WhJMwz.png" alt="download iso tool in proxmox") }} 31 32 Next to create the VM, the only thing that needs to be changed from the defaults is the memory, which I set to `8192 MB` (8 GB). 33 34 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/bDT9VdIMMG1LWvv1RwNKl.png" alt="create a vm modal in proxmox") }} 35 36 Now finish creating the VM and click on the VM after it is created. Go to options and enable start at boot. 37 38 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/ImxOHWJNuRL3yiF12cQfe.png" alt="start at boot checkbox") }} 39 40 Next, we need to pass through the physical drives to the VM. Open a terminal on the Proxmox server (use the built-in terminal or ssh in) and run the following command. Only run the part after the #. 41 ··· 118 119 Now find your VM_ID, mine is 102. 120 121 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/gwjgFbI5IrnJSTLTB0PeX.png" alt="vm list in proxmox") }} 122 123 Run the following command, replacing the VM_ID and DISK_ID with yours. 124 ··· 134 135 Here is how it appears in Proxmox: 136 137 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/QBpmqflEHmPiUHbd8JVk2.png" alt="hardware page of the vm in proxmox") }} 138 139 If everything went well, then you can start your VM now. After it finishes booting up, you will get the screen below. Make sure Install/Upgrade is selected and hit enter. 140 141 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/UFqhrRdD3GkP1_No5lWaj.png" alt="truenas startup screen") }} 142 143 You will then get this screen, use space to select the first drive and hit enter. 144 145 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/xD5QxmFtHxw10p624FgwM.png" alt="destination media screen") }} 146 147 Hit enter one last time and enter your password. 148 149 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/MZy3mN1cXBaicgVolVYs5.png" alt="confirm erase page") }} 150 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/KWq2P7Iok9LThOF5Xoj6l.png" alt="repeat password page") }} 151 152 Select BIOS, as this is the default mode for Proxmox VMs. 153 154 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/RfXwEGx6oug1vVF3UZuCj.png" alt="boot via bios or via uefi screen") }} 155 156 After about five to ten minutes, the installation process will finish and the VM will ask you to remove installation media and reboot. 157 158 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/mFEH-FHY10H7NUAvYi0aE.png" alt="installation succeded message") }} 159 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/JPXkEQJgBmeATEE40HHpr.png" alt="hardware screen in proxmox") }} 160 161 Select the installation media and remove it with the top button, go back to the console and hit enter, which will take you back to the main menu. On the main menu, select reboot with the arrow keys and hit enter. 162 163 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/IfvdMuF6AVU_f0-_rngqq.png" alt="power options screen in truenas") }} 164 165 Once the machine restarts, it will display an IP address in the console. 166 167 - {{ img(id="https://cloud-pur64l07h-hack-club-bot.vercel.app/0image.png" alt="ip address displayed in proxmox console") }} 168 169 Upon connecting to the IP address, you will get this screen. Use the root username and the password, previously configured, to login. 170 171 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/ghvCsvwAJMudUCUGvcnCu.png" alt="truenas web ui signin page") }} 172 173 Once logged in, I updated the system using the button on the home screen. 174 175 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/nrBop3a9ilvuc7h-0WPEG.png" alt="check for updates button in the truenas web ui") }} 176 177 I chose not to save the configuration file when prompted, proceeded to install the updates, and rebooted. 178 179 I hope you enjoyed the tutorial! My inspiration to make this came from watching [โ€œHow to run TrueNAS on Proxmox?โ€](https://www.youtube.com/watch?v=M3pKprTdNqQ) by [Christian Lempa](https://www.youtube.com/@christianlempa). I encourage you to watch his video if you want a video guide to installing TrueNAS on Proxmox. 180 181 - * Written on `2023-07-10` and republished to this blog (with minor edits) on `2024-10-31`
··· 6 7 [taxonomies] 8 tags = ["tutorial", "archival"] 9 +++ 10 11 + I have been using Proxmox for a while now but I've also wanted to make use of some large HDDs that have been lying around. I really didn't want to get another machine just for TrueNAS so I decided to install it on Proxmox. This is how I did it. 12 + 13 + <!-- more --> 14 + 15 + ![screenshot of the vault vm in proxmox](https://l4.dunkirk.sh/i/t-S80BbH3_js.webp){caption="my active vault storing 1.8TB of old projects"} 16 17 ## Introduction 18 ··· 28 29 Sign-in to Proxmox and upload your ISO to the local storage or, download the file directly from the link using the built-in ISO fetcher. 30 31 + ![download iso tool in proxmox](https://l4.dunkirk.sh/i/-erDtuONgTjb.webp) 32 33 Next to create the VM, the only thing that needs to be changed from the defaults is the memory, which I set to `8192 MB` (8 GB). 34 35 + ![create a vm modal in proxmox](https://l4.dunkirk.sh/i/xXwvumZox22j.webp) 36 37 Now finish creating the VM and click on the VM after it is created. Go to options and enable start at boot. 38 39 + ![start at boot checkbox](https://l4.dunkirk.sh/i/dWrsydIm5wfF.webp) 40 41 Next, we need to pass through the physical drives to the VM. Open a terminal on the Proxmox server (use the built-in terminal or ssh in) and run the following command. Only run the part after the #. 42 ··· 119 120 Now find your VM_ID, mine is 102. 121 122 + ![vm list in proxmox](https://l4.dunkirk.sh/i/kDlPW3DcBeJM.webp) 123 124 Run the following command, replacing the VM_ID and DISK_ID with yours. 125 ··· 135 136 Here is how it appears in Proxmox: 137 138 + ![hardware page of the vm in proxmox](https://l4.dunkirk.sh/i/1fUVr0Jk7wBg.webp) 139 140 If everything went well, then you can start your VM now. After it finishes booting up, you will get the screen below. Make sure Install/Upgrade is selected and hit enter. 141 142 + ![truenas startup screen](https://l4.dunkirk.sh/i/kGuQCi-UJ-XU.webp) 143 144 You will then get this screen, use space to select the first drive and hit enter. 145 146 + ![destination media screen](https://l4.dunkirk.sh/i/Z4v0J-gL1Jn9.webp) 147 148 Hit enter one last time and enter your password. 149 150 + ![confirm erase page](https://l4.dunkirk.sh/i/7Dok7mhDl-44.webp) 151 + ![repeat password page](https://l4.dunkirk.sh/i/iV97jX_E_lSM.webp) 152 153 Select BIOS, as this is the default mode for Proxmox VMs. 154 155 + ![boot via bios or via uefi screen](https://l4.dunkirk.sh/i/TJJEYiblQh5c.webp) 156 157 After about five to ten minutes, the installation process will finish and the VM will ask you to remove installation media and reboot. 158 159 + ![installation succeded message](https://l4.dunkirk.sh/i/9SAGd4cObCee.webp) 160 + ![hardware screen in proxmox](https://l4.dunkirk.sh/i/cub6hDGGZuMB.webp) 161 162 Select the installation media and remove it with the top button, go back to the console and hit enter, which will take you back to the main menu. On the main menu, select reboot with the arrow keys and hit enter. 163 164 + ![power options screen in truenas](https://l4.dunkirk.sh/i/5VpCwlveDO8d.webp) 165 166 Once the machine restarts, it will display an IP address in the console. 167 168 + ![ip address displayed in proxmox console](https://l4.dunkirk.sh/i/QZcl0rYVg8Hu.webp) 169 170 Upon connecting to the IP address, you will get this screen. Use the root username and the password, previously configured, to login. 171 172 + ![truenas web ui signin page](https://l4.dunkirk.sh/i/SUBmMtGZhoNK.webp) 173 174 Once logged in, I updated the system using the button on the home screen. 175 176 + ![check for updates button in the truenas web ui](https://l4.dunkirk.sh/i/4sumumhGS0h0.webp) 177 178 I chose not to save the configuration file when prompted, proceeded to install the updates, and rebooted. 179 180 I hope you enjoyed the tutorial! My inspiration to make this came from watching [โ€œHow to run TrueNAS on Proxmox?โ€](https://www.youtube.com/watch?v=M3pKprTdNqQ) by [Christian Lempa](https://www.youtube.com/@christianlempa). I encourage you to watch his video if you want a video guide to installing TrueNAS on Proxmox. 181 182 + * Written on `2023-07-10` and republished to this blog (with minor edits) on `2024-10-31`
+13 -14
content/blog/2023-08-04_garmin-vivoactive-homeassistant.md
··· 6 7 [taxonomies] 8 tags = ["essays", "archival"] 9 - 10 - [extra] 11 - has_toc = true 12 +++ 13 14 - {{ img(id="https://cloud-au4cbwyfl-hack-club-bot.vercel.app/0img_3051.jpg" alt="a garmin watch with the apicall app open to a spotify page" caption="I can control spotify from my watch via api hooks how bout you?") }} 15 16 - This morning I saw a [Reddit post](https://libreddit.kieranklukas.com/r/flipperzero/comments/ybjsvt/flipper_control_via_smartwatch/) where someone connected their flipper zero to a Fossil HR through [Gadgetbridge](https://gadgetbridge.org/). I immediately started [ducking,](https://libreddit.kieranklukas.com/r/duckduckgo/wiki/index#wiki_what_is_searching_on_duckduckgo_called.3F) trying to find out if I could do the same with my Garmin Vivoactive 4 but ended up realizing that there was no apparent way to connect the two. I did however find a widget compatible with my watch named [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) on the Connect IQ store. 17 18 This widget interested me because it allowed me to call any webhook I wanted utilizing the onboard Wi-Fi as well as through the Connect IQ app. This was a very important feature for me because I canโ€™t get the app to run on LineageOS as it keeps asking for the location permission even though it was already granted. 19 ··· 23 24 Now for the Google Assistant SDK / APICall / Home Assistant tutorial. The first thing you want to do is follow this guide, [Google Assistant SDK - Home Assistant](https://www.home-assistant.io/integrations/google_assistant_sdk#configuration), to install the Assistant SDK. Once you have completed that, go to Settings / Automations & Services. 25 26 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Yha1bUhOH_iuWK30QR0F1.png" alt="arrow pointing to settings in home assistant") }} 27 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/RR0VzZqsU7uTxiNlqVGum.png" alt="arrow pointing to Automations & Services in home assistant") }} 28 29 This is where you can create the action that you want to trigger with your smartwatch. The first thing you need to do is to create a new automation. Save and name the automation you just created. Now add a trigger, scroll to the bottom of the list and select webhook. If done successfully, it will look like the image below. 30 31 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/VqiM4d3wncM9BuoDR_FW7.png" alt="creating a new webhook in home assistant") }} 32 33 34 Now add an action. I decided to use the media player to play a song on Spotify. Also go back to the webhook section and click the settings icon next to the webhook ID. Change the settings to reflect below screenshot. 35 36 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/Xh3BtyMxA1MhI0rHuo3WG.png" alt="editing the webook in home assistant to allow GET queries") }} 37 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/rAbDGMrBS5fcGo7AzPT-O.png" alt="adding a play media block to the webhook") }} 38 39 Now for the fun part. Download [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) onto your Garmin smartwatch and go to the configuration section for the app. 40 41 > Note: Iโ€™ll be using Garmin Express on my MacBook, but you can also use the Garmin Connect app on a phone. 42 43 - {{ img(id="https://cloud-hsopd7dwj-hack-club-bot.vercel.app/0image.png" alt="garmin express app homepage on desktop") }} 44 45 If you are using Garmin Express, then you can access the app settings by selecting the 3 dots next to the app. You will have 36 possible API calls that you can enter. 46 47 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/-lSqNObL3TGNk0VQc8xOq.png" alt="ApiCall settings page" caption="Yes that formating is atrocious but it works at least!") }} 48 49 > webhooks 50 ```ts ··· 59 60 These are the actions that I configured for my watch so far. To customize for your API calls you need to change the `deviceName`, `actionName`, and `url` fields. The `method` and `headers` need to stay the same across all actions. If you want to add an icon to that action, then you can configure that with the `actionIcon` field. A table with the possible icons is included below, sourced from APICallโ€™s [documentation](https://apicall.dumesnil.net/documentation_en.html). 61 62 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/119m02PEgn6_wcNGtCnjM.png" alt="ApiCall icons") }} 63 64 In conclusion, you can use APICall to trigger actions in home assistant from your Garmin smartwatch. I hope this tutorial proved to be useful, and have a great rest of your day (or night). 65 66 - * Written on `2023-08-04` and republished to this blog on `2024-10-31`
··· 6 7 [taxonomies] 8 tags = ["essays", "archival"] 9 +++ 10 11 + This morning I saw a [Reddit post](https://libreddit.kieranklukas.com/r/flipperzero/comments/ybjsvt/flipper_control_via_smartwatch/) where someone connected their flipper zero to a Fossil HR through [Gadgetbridge](https://gadgetbridge.org/). I immediately started [ducking,](https://libreddit.kieranklukas.com/r/duckduckgo/wiki/index#wiki_what_is_searching_on_duckduckgo_called.3F) trying to find out if I could do the same with my Garmin Vivoactive 4 but ended up realizing that there was no apparent way to connect the two. I did however find a widget compatible with my watch named [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) on the Connect IQ store. 12 13 + <!-- more --> 14 + 15 + ![a garmin watch with the apicall app open to a spotify page](https://l4.dunkirk.sh/i/9xmYg_HE3KCn.webp){caption="I can control spotify from my watch via api hooks how bout you?"} 16 17 This widget interested me because it allowed me to call any webhook I wanted utilizing the onboard Wi-Fi as well as through the Connect IQ app. This was a very important feature for me because I canโ€™t get the app to run on LineageOS as it keeps asking for the location permission even though it was already granted. 18 ··· 22 23 Now for the Google Assistant SDK / APICall / Home Assistant tutorial. The first thing you want to do is follow this guide, [Google Assistant SDK - Home Assistant](https://www.home-assistant.io/integrations/google_assistant_sdk#configuration), to install the Assistant SDK. Once you have completed that, go to Settings / Automations & Services. 24 25 + ![arrow pointing to settings in home assistant](https://l4.dunkirk.sh/i/ibVO3Z1trZhn.webp) 26 + ![arrow pointing to Automations & Services in home assistant](https://l4.dunkirk.sh/i/ZGVec6P_sHUb.webp) 27 28 This is where you can create the action that you want to trigger with your smartwatch. The first thing you need to do is to create a new automation. Save and name the automation you just created. Now add a trigger, scroll to the bottom of the list and select webhook. If done successfully, it will look like the image below. 29 30 + ![creating a new webhook in home assistant](https://l4.dunkirk.sh/i/3EZvrvOFVNDk.webp) 31 32 33 Now add an action. I decided to use the media player to play a song on Spotify. Also go back to the webhook section and click the settings icon next to the webhook ID. Change the settings to reflect below screenshot. 34 35 + ![editing the webook in home assistant to allow GET queries](https://l4.dunkirk.sh/i/txErwYEwd9N2.webp) 36 + ![adding a play media block to the webhook](https://l4.dunkirk.sh/i/5kxiONy6zY28.webp) 37 38 Now for the fun part. Download [APICall](https://apps.garmin.com/en-US/apps/ac9a81ab-a52d-41b3-8c14-940a9de37544) onto your Garmin smartwatch and go to the configuration section for the app. 39 40 > Note: Iโ€™ll be using Garmin Express on my MacBook, but you can also use the Garmin Connect app on a phone. 41 42 + ![garmin express app homepage on desktop](https://l4.dunkirk.sh/i/7YU--efbSV7Y.webp) 43 44 If you are using Garmin Express, then you can access the app settings by selecting the 3 dots next to the app. You will have 36 possible API calls that you can enter. 45 46 + ![ApiCall settings page](https://l4.dunkirk.sh/i/yS4VBx2LyATq.webp){caption="Yes that formatting is atrocious but it works at least!"} 47 48 > webhooks 49 ```ts ··· 58 59 These are the actions that I configured for my watch so far. To customize for your API calls you need to change the `deviceName`, `actionName`, and `url` fields. The `method` and `headers` need to stay the same across all actions. If you want to add an icon to that action, then you can configure that with the `actionIcon` field. A table with the possible icons is included below, sourced from APICallโ€™s [documentation](https://apicall.dumesnil.net/documentation_en.html). 60 61 + ![ApiCall icons](https://l4.dunkirk.sh/i/c1U_5651OqAH.webp) 62 63 In conclusion, you can use APICall to trigger actions in home assistant from your Garmin smartwatch. I hope this tutorial proved to be useful, and have a great rest of your day (or night). 64 65 + * Written on `2023-08-04` and republished to this blog on `2024-10-31`
+3 -5
content/blog/2023-11-01_censorship-or-protection.md
··· 6 7 [taxonomies] 8 tags = ["essays", "archival"] 9 - 10 - [extra] 11 - has_toc = true 12 +++ 13 14 - {{ img(id="https://cloud-quuwed8n2-hack-club-bot.vercel.app/0image.png" alt="child looking out window" caption="Law makers keeping producing new โ€œonline safety billsโ€ but do they really help?") }} 15 16 17 In the last few years, we have seen a wave of โ€œonline safety billsโ€ created by lawmakers that will ostensibly help protect children online. The US has the Protecting Kids on Social Media Act (PKSMA, S.1291) and the Kids Online Safety Act (KOSA, S.1409) while in the UK they have the Online Safety Bill (OSB). The main feature that all of these bills have in common is the censorship of online content for minors. The Electronic Frontier Foundation (EFF) has raised concerns over KOSA, saying, โ€œThe bill requires all websites, apps, and online platforms to filter and block legal speechโ€ (Mullin). These bills raise an important questionโ€“should the government regulate the online activities of children, or should that responsibility lie solely with parents? 18 ··· 58 59 Wisniewski, Pamela J., et al. โ€œPrivacy in Adolescence.โ€ _Modern Socio-Technical Perspectives on Privacy_, edited by Bart P. Knijnenburg et al., Springer International Publishing, 2022, pp. 315โ€“36. _Springer Link_, [](https://doi.org/10.1007/978-3-030-82786-1_14)[https://doi.org/10.1007/978-3-030-82786-1_14](https://www.brennancenter.org/our-work/research-reports/citizens-without-proof). 60 61 - * Written on `2023-11-01` and republished to this blog on `2024-10-31`
··· 6 7 [taxonomies] 8 tags = ["essays", "archival"] 9 +++ 10 11 + <!-- more --> 12 13 + ![child looking out window](https://l4.dunkirk.sh/i/ftoCrqy9S3-o.webp){caption="Law makers keeping producing new โ€œonline safety billsโ€ but do they really help?"} 14 15 In the last few years, we have seen a wave of โ€œonline safety billsโ€ created by lawmakers that will ostensibly help protect children online. The US has the Protecting Kids on Social Media Act (PKSMA, S.1291) and the Kids Online Safety Act (KOSA, S.1409) while in the UK they have the Online Safety Bill (OSB). The main feature that all of these bills have in common is the censorship of online content for minors. The Electronic Frontier Foundation (EFF) has raised concerns over KOSA, saying, โ€œThe bill requires all websites, apps, and online platforms to filter and block legal speechโ€ (Mullin). These bills raise an important questionโ€“should the government regulate the online activities of children, or should that responsibility lie solely with parents? 16 ··· 56 57 Wisniewski, Pamela J., et al. โ€œPrivacy in Adolescence.โ€ _Modern Socio-Technical Perspectives on Privacy_, edited by Bart P. Knijnenburg et al., Springer International Publishing, 2022, pp. 315โ€“36. _Springer Link_, [](https://doi.org/10.1007/978-3-030-82786-1_14)[https://doi.org/10.1007/978-3-030-82786-1_14](https://www.brennancenter.org/our-work/research-reports/citizens-without-proof). 58 59 + * Written on `2023-11-01` and republished to this blog on `2024-10-31`
+102
content/blog/2023-11-10_monaspace-vs-code-install.md
···
··· 1 + +++ 2 + title = "Monaspace VS-Code install" 3 + date = 2023-11-10 4 + slug = "monaspace-vs-code-install" 5 + description = "How to install the Github Next team's Monaspace font in VSCode" 6 + 7 + [taxonomies] 8 + tags = ["tutorial", "archival"] 9 + +++ 10 + 11 + To install the Monaspace font on macOS (or windows or linux) with VS Code and enable multifont syntax highlighting with the [CSS JS Loader extension](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css), you can follow these steps: 12 + 13 + <!-- more --> 14 + 15 + ![monaspace font in action](https://l4.dunkirk.sh/i/Y4cEqfeAHBYR.webp){caption="This font is so pretty and has so many features its amazing. It's main downside is to work it takes to set it up."} 16 + 17 + 18 + ## 1. Download and install the Monaspace font: 19 + 20 + First visit [https://github.com/githubnext/monaspace/releases/latest](https://github.com/githubnext/monaspace/releases/latest) and download the zip. 21 + Next to install the Monaspace font: 22 + - On macOS, drag the font files into font book. 23 + - For windows, drag into the font window in settings. 24 + - For Linux, clone the repo and run: `cd util; ./install_linux.sh` 25 + 26 + ## 2. Configure VS Code 27 + 28 + Install the [Custom CSS and JS Loader](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css) plugin. 29 + Set the font to one of the following options: `Monaspace Neon Var`, `Monaspace Argon Var`, `Monaspace Xeon Var`, `Monaspace Radon Var`, or `Monaspace Krypton Var`. 30 + 31 + - You will find this option under _Editor: Font Family_ in the user preferences 32 + 33 + ![the available varients of the font](https://l4.dunkirk.sh/i/JZ0hERyiZhmR.webp) 34 + 35 + 36 + Next enable font ligatures in the settings.json with following snippet: 37 + 38 + > settings.json 39 + ```json 40 + "editor.fontLigatures": "'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', calt', 'dlig'", 41 + ``` 42 + Now enable the custom CSS file within the `settings.json`, modifying the file path for Windows / MacOS / Linux if needed: 43 + 44 + > still settings.json 45 + ```json 46 + "vscode_custom_css.imports": [ 47 + "file:///Users/{{user}}/.vscode/style.css", // for mac (remove if not mac) 48 + "file://C://Users/{{user}}/vscode/style.css" // for windows (remove if not windows) 49 + "file:///home/{{user}}/.vscode/style.css" // for linux (remove if not windows) 50 + ], 51 + ``` 52 + 53 + ## 3. Create custom CSS file at the path you specified above. 54 + 55 + Depending on your VS Code version, the class names might be different, so you may need to use the developer tools to find the correct one. 56 + The styles that worked for me on `VS Code version: 1.84.2 (Universal) commit: 1a5daa3a0231a0fbba4f14db7ec463cf99d7768e` are here: 57 + 58 + > style.css 59 + ```css 60 + /* Comment Class */ 61 + .mtk3 { 62 + font-family: "Monaspace Radon Var"; 63 + font-weight: 500; 64 + } 65 + 66 + /* Copilot Classes */ 67 + .ghost-text-decoration { 68 + font-family: "Monaspace Krypton Var"; 69 + font-weight: 200; 70 + } 71 + 72 + .ghost-text-decoration-preview { 73 + font-family: "Monaspace Krypton Var"; 74 + font-weight: 200; 75 + } 76 + ``` 77 + 78 + *Thanks to **[@fspoettel](https://github.com/fspoettel)** on GitHub for this trick to get the copilot classes when in dev mode* 79 + 80 + > "You can inspect transient DOM elements by halting the app with a `debugger` after a delay with a debugger call inside a `setTimeout`." 81 + > 82 + > <cite>[@fspoettel](https://github.com/fspoettel)</cite> 83 + 84 + You can copy the following snippet to do just that! 85 + 86 + > console 87 + ```ts 88 + setTimeout(() => { 89 + debugger; 90 + }, 10000); 91 + ``` 92 + 93 + Before you are finished make sure you have run the `Enable Custom CSS and JS` command from the command bar. 94 + 95 + ## Closing Remarks 96 + 97 + That should be it! Hopefully you will have a beautiful custom font VS Code install. 98 + 99 + If you are looking for a good theme, I can highly recommend the [Catppuccin](https://marketplace.visualstudio.com/items?itemName=Catppuccin.catppuccin-vsc) theme, as that is what I use myself. Be sure to check out [Monaspaceโ€™s website](https://monaspace.githubnext.com/) as it is a work of art. Happy Coding! ๐Ÿ‘ฉโ€๐Ÿ’ป 100 + 101 + * *Updated 2024-08-22: changed mtk4 to mtk3 on the feedback of [mutammim](https://github.com/mutammim)* 102 + * *Updated 2024-10-31: changed around the formatting of the post and moved to [dunkirk.sh](https://dunkirk.sh)*
+12 -13
content/blog/2024-08-03_ssd-removal-mbp-2017.md
··· 6 7 [taxonomies] 8 tags = ["tutorial", "teardown", "archival"] 9 10 - [extra] 11 - has_toc = true 12 - +++ 13 14 - {{ img(id="https://cloud-owp7vmln1-hack-club-bot.vercel.app/0img_1846_1_.jpg" alt="MacBook proprietary blade SSD" caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage") }} 15 16 - Hi! I've had a MacBook Pro 2017 for about a year now, and I got it used; it's been great so far until one day after updating it just refused to turn on I'm not entirely sure why this happened, but I replaced the battery and that didn't solve the issue so yeah ^_^ 17 18 I eventually decided to just try and remove the SSD from the MacBook and see if there was a way to recover any files from it (spoiler: there kinda is, but it's annoying) but I couldn't find any guide online and iFixit had nothing. So I decided to just try and yolo it and see if I could figure it out on my own, and surprisingly I actually managed to do it! Turns out, the process isn't that hard! I'll take you through the steps I took so that if you want to do this, it's much less of a hassle. 19 20 ## Guide 21 22 1. the first thing you need to do is to remove the screws from the back of your MacBook. This will use a P5 Pentalobe driver, which I believe you can buy from iFixit as well as several other companies on Amazon. 23 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/1img_1838.jpg", alt="Removing the screws") }} 24 25 1. next you need to crack open the shell of the MacBook by prying under the front (on the side where the MacBook opens). It's pretty helpful to have a suction cup or something to lift it up a bit so you can get your prying tool underneath (I used a flat plastic prying tool I got from the battery repair kit for this MacBook, but a guitar pick or credit card would probably also work) 26 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/2img_1839.jpg", alt="using a suction cup to lift the back shell") }} 27 28 1. now once you've got the back slightly opened up just run around the edge of the shell prying up on it until the front and two sides are free then just pull forward at a slight (15ish degree?) angle, and it should slide right out. 29 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/3img_1840.jpg", alt="the opened MacBook") }} 30 31 1. once it's open, locate the silver metal block looking thing; this is your SSD 32 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/4img_1841.jpg", alt="the SSD") }} 33 34 1. now using a T5 Torx driver (why couldn't you just use one type of screws apple ๐Ÿ˜ญ; be more like framework) you need to unscrew the two screws on either side of the front of the SSD 35 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/7img_1844.jpg", alt="the screws") }} 36 37 1. now comes the slightly scary part (for me at least) you need to lift the black tape that's covering the top of the SSD (don't worry the SSD will be fine) 38 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/8img_1845.jpg", alt="the removed tape on the SSD") }} 39 40 1. now just slightly pull on the SSD (again at a slight angle) and it should pop right out! 41 - {{ img(id="https://cloud-nw5fqpqfw-hack-club-bot.vercel.app/9img_1846.jpg", alt="the SSD out of the MacBook") }} 42 43 ## Postlog and notes 44 45 I hope this helped if you are trying to do this your self! Now for recovering the data the two options I've found are a) buy a secondary MacBook of the exact same generation and model and swap your SSD in or b) pay some data recovery company a lot of money to probably do the same thing for you; neither option is super appealing to me, so I'll keep searching for alternatives and I will be sure to update this article if I do find any. As of today though (August 3rd 2024) I haven't been able to get a hold of another MacBook or adaptor to connect this to my computer but if you do find one definitely leave a comment on the hacker news post linked below! 46 47 * Posted on HackerNews on `2024-08-03` [hn://item/41147359](https://news.ycombinator.com/item?id=41147359) 48 - * Republished to this blog on `2024-10-31` with minor edits
··· 6 7 [taxonomies] 8 tags = ["tutorial", "teardown", "archival"] 9 + +++ 10 11 + Hi! I've had a MacBook Pro 2017 for about a year now, and I got it used; it's been great so far until one day after updating it just refused to turn on I'm not entirely sure why this happened, but I replaced the battery and that didn't solve the issue so yeah ^_^ 12 13 + <!-- more --> 14 15 + ![MacBook proprietary blade SSD](https://l4.dunkirk.sh/i/REmc3Tnp43hn.webp){caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage"} 16 17 I eventually decided to just try and remove the SSD from the MacBook and see if there was a way to recover any files from it (spoiler: there kinda is, but it's annoying) but I couldn't find any guide online and iFixit had nothing. So I decided to just try and yolo it and see if I could figure it out on my own, and surprisingly I actually managed to do it! Turns out, the process isn't that hard! I'll take you through the steps I took so that if you want to do this, it's much less of a hassle. 18 19 ## Guide 20 21 1. the first thing you need to do is to remove the screws from the back of your MacBook. This will use a P5 Pentalobe driver, which I believe you can buy from iFixit as well as several other companies on Amazon. 22 + ![Removing the screws](https://l4.dunkirk.sh/i/QtJUiXMSX-lY.webp) 23 24 1. next you need to crack open the shell of the MacBook by prying under the front (on the side where the MacBook opens). It's pretty helpful to have a suction cup or something to lift it up a bit so you can get your prying tool underneath (I used a flat plastic prying tool I got from the battery repair kit for this MacBook, but a guitar pick or credit card would probably also work) 25 + ![using a suction cup to lift the back shell](https://l4.dunkirk.sh/i/GdbMMR4JBh6x.webp) 26 27 1. now once you've got the back slightly opened up just run around the edge of the shell prying up on it until the front and two sides are free then just pull forward at a slight (15ish degree?) angle, and it should slide right out. 28 + ![the opened MacBook](https://l4.dunkirk.sh/i/vrohFehEFZ0r.webp) 29 30 1. once it's open, locate the silver metal block looking thing; this is your SSD 31 + ![the SSD](https://l4.dunkirk.sh/i/y2s-Zcdt-FL6.webp) 32 33 1. now using a T5 Torx driver (why couldn't you just use one type of screws apple ๐Ÿ˜ญ; be more like framework) you need to unscrew the two screws on either side of the front of the SSD 34 + ![the screws](https://l4.dunkirk.sh/i/brqIWi0VeSAX.webp) 35 36 1. now comes the slightly scary part (for me at least) you need to lift the black tape that's covering the top of the SSD (don't worry the SSD will be fine) 37 + ![the removed tape on the SSD](https://l4.dunkirk.sh/i/aG0xcxZMN9Kq.webp) 38 39 1. now just slightly pull on the SSD (again at a slight angle) and it should pop right out! 40 + ![the SSD out of the MacBook](https://l4.dunkirk.sh/i/RlA6xfMC8qdi.webp) 41 42 ## Postlog and notes 43 44 I hope this helped if you are trying to do this your self! Now for recovering the data the two options I've found are a) buy a secondary MacBook of the exact same generation and model and swap your SSD in or b) pay some data recovery company a lot of money to probably do the same thing for you; neither option is super appealing to me, so I'll keep searching for alternatives and I will be sure to update this article if I do find any. As of today though (August 3rd 2024) I haven't been able to get a hold of another MacBook or adaptor to connect this to my computer but if you do find one definitely leave a comment on the hacker news post linked below! 45 46 * Posted on HackerNews on `2024-08-03` [hn://item/41147359](https://news.ycombinator.com/item?id=41147359) 47 + * Republished to this blog on `2024-10-31` with minor edits
+72 -85
content/blog/2024-10-11_example_post.md
··· 1 +++ 2 - title = "Test Post" 3 date = 2024-10-11 4 - slug = "test-post" 5 - description = "Testing out styling and features." 6 7 [taxonomies] 8 tags = ["meta"] ··· 15 make sure that if I change the CSS or anything I don't break any of it! This is also a 16 sort of light style guide for blog posts in general. 17 18 ## Section Headers 19 20 Sections headers (prefixed with `##` in markdown) are the main content separators for posts, and 21 can be [linked to](#section-headers) directly. To link to them, the header's text needs to be 22 *kebab-cased*, so the above would be `#section-headers`. 23 24 - ### But what about sub-headers? 25 - 26 - I usually use `###` sub-headers to ask the question I think the reader is (or should be) asking at 27 - this point in the article. For example, if I just posted some code with an obvious error, I might 28 - follow that up with `### Wait, won't that crash?` or something similar. Using this approach lets 29 - me write posts in a conversational way, and helps me continually frame myself in the mind of the 30 - reader. 31 32 ### Table of Contents 33 ··· 40 has_toc = true 41 ``` 42 43 - I don't like content that is nested more than 2 layers deep, so only `##` and `###` should be used 44 - to divide things up. 45 46 ## Embedding Code 47 48 - This is prominently a coding blog, so code will show up a lot. First off, a monospaced text block is 49 - denoted by wrapping the text in triple back-tick characters <code>&#x0060;&#x0060;&#x0060;</code>. 50 - 51 - ``` 52 - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 53 - โ”‚ This text is monospaced. โ”‚ 54 - โ”‚ This โ”‚ 55 - โ”‚ text โ”‚ 56 - โ”‚ is โ”‚ 57 - โ”‚ monospaced. โ”‚ 58 - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 59 - ``` 60 61 ### Syntax Highlighting 62 63 If you want syntax coloring, you put the name of the programming language immediately after the ticks. 64 So writing this: 65 66 - ~~~ 67 ```rust 68 fn main() { 69 println!("Hello, world!"); ··· 83 84 Sometimes it can help to give a header to a code block to signal what it represents. To do this, you put 85 a single-line block quote immediately before the code block. So by prepending the following code with 86 - `> src/main.rs`, I can produce this: 87 88 - > src/main.rs 89 - ```rust 90 - fn main() { 91 - println!("This code is in main.rs!"); 92 - } 93 - ``` 94 - 95 - This can be useful to explicitly state the programming language or format being used: 96 - 97 - > TOML 98 - ```toml 99 - title = "Test Post" 100 - slug = "test-post" 101 - description = "Testing out styling and features." 102 - 103 - [taxonomies] 104 - tags = ["meta"] 105 ``` 106 107 ### Inline Code 108 109 As seen above, sometimes code items are mentioned in regular paragraphs, but you want to 110 - draw attention to them. To do this, you can wrap it in &#x0060; back-tick quotes. For 111 example, if I wanted to mention Rust's `Vec<T>` type. 112 113 ```md ··· 129 130 > "This text will appear italicized in a quote box!" 131 132 - ### Reader Questions 133 - 134 - When displaying reader questions, I start the block quote with a bolded name, like so: 135 - 136 - > **SonicFan420x69 asks:** 137 - > 138 - > &ldquo;What is your opinion of the inimitable video game character, Sonic the Hedgehog? Please 139 - > answer soon as it is a matter of life or death.&rdquo; 140 - 141 ### Cited Quotations 142 143 For when I want to have a citation, I can use the html `<cite>` tag after the quote text and it ··· 148 > 149 > <cite>Bilbo Baggins</cite> 150 151 - ## Icons &amp; Images 152 - 153 - They were shown in the previous section, but icons (provided by [Remix Icon](https://remixicon.com/)), 154 - can be used anywhere by inserting an `<i>` tag with the icon's class. These are useful for adding 155 - some detail and decorating to the pages, and is another way to break up text. 156 - 157 ## Embedding Media 158 159 Images and videos are a great way to break up content and prevent text fatigue. ··· 166 ![alt text](/path/to/image.png) 167 ``` 168 169 - ![NOISE1 screenshot](https://img.itch.zone/aW1hZ2UvNTU2NDU0LzI5MTYzNzgucG5n/original/6GRlJM.png) 170 171 When there are multiple paragraphs of text in a row (usually 3-4), and nothing else to break 172 them up, images can be interspersed to help prevent text-wall fatique. 173 174 You can also add captions to images: 175 176 - <figure> 177 - <img src="https://img.itch.zone/aW1hZ2UvNTU2NDU0LzI5MTYzNzkucG5n/original/8LIdCb.png" alt="NOISE1 screenshot"> 178 - <figcaption> 179 - NOISE1 is a dark sci-fi hacker-typing stealth game. 180 - </figcaption> 181 - </figure> 182 183 - But there is no way to do this in markdown so you have to use the `<figure>` tag like so: 184 185 - ```html 186 - <figure> 187 - <img src="/path/to/image.png" alt="Alt text goes here."> 188 - <figcaption>Caption text goes here.</figcaption> 189 - </figure> 190 ``` 191 192 ### Videos 193 194 - To embed a video, you use the `youtube` shortcode e.g. 195 - 196 - > post.md 197 - ```md 198 - {{/* youtube(id="kiWvNwuBbEE") */}} 199 - ``` 200 - 201 - You can also add the `autoplay=true` flag to make the video autoplay. 202 203 {{ youtube(id="NodwjZF7uZw") }} 204 205 - The shortcode is processed into an iframe which looks like this: 206 207 - > post.html 208 - ```html 209 - {{ youtube(id="kiWvNwuBbEE") }} 210 - ``` 211 212 ## Miscellaneous 213 ··· 216 --- 217 218 But these should be used sparingly, if at all.
··· 1 +++ 2 + title = "The *Mega* test case" 3 date = 2024-10-11 4 + slug = "mega" 5 + description = "How I write / leme check if that broke anything page" 6 7 [taxonomies] 8 tags = ["meta"] ··· 15 make sure that if I change the CSS or anything I don't break any of it! This is also a 16 sort of light style guide for blog posts in general. 17 18 + <!-- more --> 19 + 20 ## Section Headers 21 22 Sections headers (prefixed with `##` in markdown) are the main content separators for posts, and 23 can be [linked to](#section-headers) directly. To link to them, the header's text needs to be 24 *kebab-cased*, so the above would be `#section-headers`. 25 26 + Not quite a section header, the `<!-- more -->` tag is used to indicate where a post should be split for rss purposes. This should generally be right after the first paragraph. 27 28 ### Table of Contents 29 ··· 36 has_toc = true 37 ``` 38 39 + The table of contents will only ever be generated for `##` and `###` headers. I don't particularly love the look of it and tend to write shorter posts so I hardly use it. 40 41 ## Embedding Code 42 43 + I tend to do this alot so this is an important bit of the blog. All code blocks with a code type are progressively enhanced with a copy button. 44 45 ### Syntax Highlighting 46 47 If you want syntax coloring, you put the name of the programming language immediately after the ticks. 48 So writing this: 49 50 + ~~~md 51 ```rust 52 fn main() { 53 println!("Hello, world!"); ··· 67 68 Sometimes it can help to give a header to a code block to signal what it represents. To do this, you put 69 a single-line block quote immediately before the code block. So by prepending the following code with 70 + `> src/index.ts`, I can produce this: 71 72 + > src/index.ts 73 + ```ts 74 + Bun.serve({ 75 + port: 3000, 76 + fetch(req) { 77 + return new Response("Hello, world!"); 78 + } 79 + }); 80 ``` 81 82 ### Inline Code 83 84 As seen above, sometimes code items are mentioned in regular paragraphs, but you want to 85 + draw attention to them. To do this, you can wrap it in back-tick (\`) quotes. For 86 example, if I wanted to mention Rust's `Vec<T>` type. 87 88 ```md ··· 104 105 > "This text will appear italicized in a quote box!" 106 107 ### Cited Quotations 108 109 For when I want to have a citation, I can use the html `<cite>` tag after the quote text and it ··· 114 > 115 > <cite>Bilbo Baggins</cite> 116 117 ## Embedding Media 118 119 Images and videos are a great way to break up content and prevent text fatigue. ··· 126 ![alt text](/path/to/image.png) 127 ``` 128 129 + ![NOISE1 screenshot](https://l4.dunkirk.sh/i/MZp-ye7LclCx.webp) 130 131 When there are multiple paragraphs of text in a row (usually 3-4), and nothing else to break 132 them up, images can be interspersed to help prevent text-wall fatique. 133 134 You can also add captions to images: 135 136 + ```terra 137 + {{/* img(id="https://url.com/image.png" alt="alt text" caption="this can be ommited if you want or added! It's optional :)") */}} 138 + ``` 139 140 + ![MacBook proprietary blade SSD](https://l4.dunkirk.sh/i/REmc3Tnp43hn.webp){caption="it really was a rather sleek design; shame that apple got rid of it in favor of soldered on storage"} 141 142 + You can also display multiple images side-by-side using the `imgs` shortcode with comma-separated URLs: 143 + 144 + ```terra 145 + {{/* imgs(id="https://url.com/image1.png, https://url.com/image2.png" alt="alt text 1, alt text 2" caption="optional caption for both images") */}} 146 ``` 147 + 148 + !![the copyright section](https://l4.dunkirk.sh/i/FPBdusjL9oIZ.webp)[the ssh section](https://l4.dunkirk.sh/i/FCjVs9QyX8jd.webp){caption="side by side images from the remarkable tutorial"} 149 150 ### Videos 151 152 + To embed a video, you use the `youtube(id="", autoplay?=bool)` shortcode e.g. 153 154 {{ youtube(id="NodwjZF7uZw") }} 155 156 + ### Bluesky posts 157 158 + This is handled by a shortcode `bluesky(post="")` and takes the post url as a parameter. These will automatically attach images and videos. 159 + 160 + {{ bluesky(post="https://bsky.app/profile/svenninifl.bsky.social/post/3lnkivz3ans2k") }} 161 162 ## Miscellaneous 163 ··· 166 --- 167 168 But these should be used sparingly, if at all. 169 + 170 + You can also use emojis inline from the hackclub slack like this :yay:! This is just done by writing `:emoji:` and it gets progressively enhanced with a bit of js as long as the emoji is in cachet! 171 + 172 + ## Callouts 173 + 174 + Callouts are a great way to draw attention to important information. They come in several types: 175 + 176 + ### Info Callout 177 + 178 + > [!INFO] 179 + > This is an info callout! Use this for general information that readers should be aware of. 180 + 181 + ### Warning Callout 182 + 183 + > [!WARNING] 184 + > This is a warning callout! Use this to alert readers about potential issues or things to watch out for. 185 + 186 + ### Danger Callout 187 + 188 + > [!DANGER] 189 + > This is a danger callout! Use this for critical information that could cause problems if ignored. 190 + 191 + ### Tip Callout 192 + 193 + > [!TIP] 194 + > This is a tip callout! Use this to share helpful hints and best practices. 195 + 196 + ### Note Callout 197 + 198 + > [!NOTE] 199 + > This is a note callout! Use this for additional context or side information. 200 + 201 + ### Custom Title 202 + 203 + {% callout(type="info", title="Custom Title Here") %} 204 + You can also customize the title of any callout by adding a `title` parameter! 205 + {% end %}
+7 -8
content/blog/2024-10-13_hilton_tomfoolery.md
··· 6 7 [taxonomies] 8 tags = ["reverse engineering", "hilton"] 9 - 10 - [extra] 11 - has_toc = true 12 +++ 13 14 I'm at a Hilton at the time of writing this, and I'm decently bored. Currently, I'm downloading the latest version of RogueMaster (0.420.0) to my flipper, as it is currently crashing every time I open the NFC app. My dad tried out the app unlock feature in the Hilton app for the first time today, which, as most new tech things, made me quite curious how it worked and whether I could break it. Based on playing with it, there seems to be a proximity reading (over Bluetooth? Perhaps a BLE beacon?) to detect if you are by your door but for a period of time (~20 sec) after getting that signal it allows you to unlock the door from across the room which I'm guessing means that it controls the locks via a central server. The current plan is to install the root cert (of mitmproxy) on my iPhone and then try and intercept those API calls and see if we can manipulate them in any interesting ways. I'm also planning on live blogging this, which I've never tried before. (I also wrote this whole article in vim ^_^) 15 16 ## Connecting to Mitmproxy 17 18 I'm connecting over WireGuard, so I fired up mitmproxy with `mitmweb --mode wireguard` on my laptop. Connecting via WireGuard theoretically is pretty simple; all I need to do is to scan a qr code and connect. Unfortunately, the hotel Wi-Fi seems to be oddly segmented, and I can't access the WireGuard server or ping my laptop from my phone. I'm going to try firing up a hot spot on my dad's phone and see if that allows me to talk to my phone. 19 20 - {{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/2install_profile.png" alt="screenshot of the root certificate install process" caption="You have to dig through several menus to trust it") }} 21 22 I messed with getting my laptop to connect to my dad's phone, but it kept refusing for some reason. My next idea is to ngrok the WireGuard tunnel, which ended up failing because ngrok doesn't support UDP. Finally, after an embarrassingly long time, I realized that I could simply use `ngrok tcp 8080` and the HTTP proxy server built into mitmproxy instead. After installing the root certificate and trusting it in the iPhone settings, we were good to go! 23 ··· 759 760 ## Locks 761 762 - {{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/0hotel-key.png" alt="screenshot of the hotel digital key" caption="What it looks like in the app") }} 763 764 When using the unlock button, it made a request to this URL: `https://smetric.hilton.com/b/ss/hiltonglobalprod/10/IOSN030200030900/s65425920` with a payload of a URL encoded form. 765 ··· 934 935 ## Wrap up 936 937 - {{ img(id="https://cloud-ryjlxhb9r-hack-club-bot.vercel.app/1bluetooth-scan.png" alt="screenshot of bluetooth scan" caption="The bluetooth scan of (what i belive is) the lift") }} 938 939 I tried running a Bluetooth scan to see if I could find the locks, but nothing popped out as being a likely culprit. I did however find an interesting set of 3 Bluetooth devices named "clearsky smart fleet" which upon research seems to be scissor lifts / construction equipment made by a company called [JLG](https://smartfleet.jlg.com/) which is quite interesting. That would make sense, however, as I saw several scissor lifts outside the hotel on my way in. 940 941 - {{ img(id="https://cloud-1asinv8kn-hack-club-bot.vercel.app/0img_2781.jpg" alt="image of JLG lift" caption="The same (probably) JLG lift in the wild!") }} 942 943 By the time I'm writing this it's 6:41, and I need to eat breakfast, so I'll probably finish this post in the car this afternoon. Overall this was a fascinating experiment and while I sadly did fail at unlocking doors from my laptop I do feel more confident with reverse engineering app requests now! The next step would probably be to grab the app bundle and try to decompile it looking for the URLs we saw, but I don't have a mac on me, and I've never done that before. Next post? 944 945 Taking inspiration from the [LOWโ†TECH MAGAZINE](https://solar.lowtechmagazine.com/) I will be taking any questions / comments about this article via email and then posting them here to my site! If you have a question or comment, feel free to email me at [me@dunkirk.sh](mailto://me@dunkirk.sh). Now to go eat breakfast :) 946 947 - {{ img(id="https://cloud-1asinv8kn-hack-club-bot.vercel.app/3img_2777.jpg" alt="image of my hotel breakfast" caption="A delicious waffle, mildy warm bacon, and under seasoned potatoes.") }}
··· 6 7 [taxonomies] 8 tags = ["reverse engineering", "hilton"] 9 +++ 10 11 I'm at a Hilton at the time of writing this, and I'm decently bored. Currently, I'm downloading the latest version of RogueMaster (0.420.0) to my flipper, as it is currently crashing every time I open the NFC app. My dad tried out the app unlock feature in the Hilton app for the first time today, which, as most new tech things, made me quite curious how it worked and whether I could break it. Based on playing with it, there seems to be a proximity reading (over Bluetooth? Perhaps a BLE beacon?) to detect if you are by your door but for a period of time (~20 sec) after getting that signal it allows you to unlock the door from across the room which I'm guessing means that it controls the locks via a central server. The current plan is to install the root cert (of mitmproxy) on my iPhone and then try and intercept those API calls and see if we can manipulate them in any interesting ways. I'm also planning on live blogging this, which I've never tried before. (I also wrote this whole article in vim ^_^) 12 13 + <!-- more --> 14 + 15 ## Connecting to Mitmproxy 16 17 I'm connecting over WireGuard, so I fired up mitmproxy with `mitmweb --mode wireguard` on my laptop. Connecting via WireGuard theoretically is pretty simple; all I need to do is to scan a qr code and connect. Unfortunately, the hotel Wi-Fi seems to be oddly segmented, and I can't access the WireGuard server or ping my laptop from my phone. I'm going to try firing up a hot spot on my dad's phone and see if that allows me to talk to my phone. 18 19 + ![screenshot of the root certificate install process](https://l4.dunkirk.sh/i/iYaDaVPYV1Lc.webp){caption="You have to dig through several menus to trust it"} 20 21 I messed with getting my laptop to connect to my dad's phone, but it kept refusing for some reason. My next idea is to ngrok the WireGuard tunnel, which ended up failing because ngrok doesn't support UDP. Finally, after an embarrassingly long time, I realized that I could simply use `ngrok tcp 8080` and the HTTP proxy server built into mitmproxy instead. After installing the root certificate and trusting it in the iPhone settings, we were good to go! 22 ··· 758 759 ## Locks 760 761 + ![screenshot of the hotel digital key](https://l4.dunkirk.sh/i/JfDm-stHm3hH.webp){caption="What it looks like in the app"} 762 763 When using the unlock button, it made a request to this URL: `https://smetric.hilton.com/b/ss/hiltonglobalprod/10/IOSN030200030900/s65425920` with a payload of a URL encoded form. 764 ··· 933 934 ## Wrap up 935 936 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/4e9bfb28c266eb29cea1568cedd3573be2ba1f97_1bluetooth-scan.png" alt="screenshot of bluetooth scan" caption="The bluetooth scan of (what i believe is) the lift") }} 937 938 I tried running a Bluetooth scan to see if I could find the locks, but nothing popped out as being a likely culprit. I did however find an interesting set of 3 Bluetooth devices named "clearsky smart fleet" which upon research seems to be scissor lifts / construction equipment made by a company called [JLG](https://smartfleet.jlg.com/) which is quite interesting. That would make sense, however, as I saw several scissor lifts outside the hotel on my way in. 939 940 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/993ad810e42289ad3aaefa4093ede271a4ee1d12_0img_2781.jpg" alt="image of JLG lift" caption="The same (probably) JLG lift in the wild!") }} 941 942 By the time I'm writing this it's 6:41, and I need to eat breakfast, so I'll probably finish this post in the car this afternoon. Overall this was a fascinating experiment and while I sadly did fail at unlocking doors from my laptop I do feel more confident with reverse engineering app requests now! The next step would probably be to grab the app bundle and try to decompile it looking for the URLs we saw, but I don't have a mac on me, and I've never done that before. Next post? 943 944 Taking inspiration from the [LOWโ†TECH MAGAZINE](https://solar.lowtechmagazine.com/) I will be taking any questions / comments about this article via email and then posting them here to my site! If you have a question or comment, feel free to email me at [me@dunkirk.sh](mailto://me@dunkirk.sh). Now to go eat breakfast :) 945 946 + ![image of my hotel breakfast](https://l4.dunkirk.sh/i/CNEMtC8HNWRs.webp){caption="A delicious waffle, mildy warm bacon, and under seasoned potatoes."}
+5 -6
content/blog/2024-10-23_hilton_decompilation.md
··· 7 8 [taxonomies] 9 tags = ["reverse engineering", "hilton"] 10 - 11 - [extra] 12 - has_toc = true 13 +++ 14 15 Ello! I'm back again! I'll be staying at a Hotel again in two days so I decided to try to decompile the app ahead of time so I can test stuff while I'm there. I decided to target the android app first because it seemed easier to decompile (i've partly decompiled an apk before about 3 and half years ago to embed a payload in it and I don't remember it being horrible) and I knew getting the apk itself would be far easier than from the Apple App Store. 16 17 - {{ img(id="https://cloud-glc3mgu9t-hack-club-bot.vercel.app/0image.png" alt="screenshot of the nix packages entry" caption="prepackaged for nix; always a good sign") }} 18 19 I was able to download the apk from the [apkcombo.com](https://apkcombo.com/downloader/#package=com.hilton.android.hhonors) website by simply inputing the play store URL so we were off to a good start. Apktool was already in [nix packages](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=apktool) so we didn't have to do anything fancy there. One `pkgs.unstable.apktool` and a `sudo nixos-rebuild switch` latter and we were ready to go. Then I waited another 2 days lol. Finally in the hotel room (again crunched on time; why do I never seem to learn?) I was able to decompile the apk and start looking around. 20 21 - {{ img(id="https://cloud-qh7hbvivt-hack-club-bot.vercel.app/0image.png" alt="screenshot of the successful decompilation process" caption="all nicely decompiled") }} 22 23 - I started uploading the decompiled app to github ([kcoderhtml/hilton-honors](https://github.com/kcoderhtml/hilton-honors)) which was incredibly slow and then started poking around the app. The first thing I noticed was quite a few files with firebase in the name as well as several play store properties files. All of them seemed to follow the same pattern of having a `version`, `client`, and then file specific client key. 24 25 ```text 26 $ ls unknown/firebase*
··· 7 8 [taxonomies] 9 tags = ["reverse engineering", "hilton"] 10 +++ 11 12 Ello! I'm back again! I'll be staying at a Hotel again in two days so I decided to try to decompile the app ahead of time so I can test stuff while I'm there. I decided to target the android app first because it seemed easier to decompile (i've partly decompiled an apk before about 3 and half years ago to embed a payload in it and I don't remember it being horrible) and I knew getting the apk itself would be far easier than from the Apple App Store. 13 14 + <!-- more --> 15 + 16 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/4e667b8066044667ea63d5ec44222aef97dc764c_0image.png" alt="screenshot of the nix packages entry" caption="prepackaged for nix; always a good sign") }} 17 18 I was able to download the apk from the [apkcombo.com](https://apkcombo.com/downloader/#package=com.hilton.android.hhonors) website by simply inputing the play store URL so we were off to a good start. Apktool was already in [nix packages](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=apktool) so we didn't have to do anything fancy there. One `pkgs.unstable.apktool` and a `sudo nixos-rebuild switch` latter and we were ready to go. Then I waited another 2 days lol. Finally in the hotel room (again crunched on time; why do I never seem to learn?) I was able to decompile the apk and start looking around. 19 20 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/55f3ffe6a3f8130fc7f389d5d151660364e99d93_0image.png" alt="screenshot of the successful decompilation process" caption="all nicely decompiled") }} 21 22 + I started uploading the decompiled app to github ([taciturnaxolotl/hilton-honors](https://github.com/taciturnaxolotl/hilton-honors)) which was incredibly slow and then started poking around the app. The first thing I noticed was quite a few files with firebase in the name as well as several play store properties files. All of them seemed to follow the same pattern of having a `version`, `client`, and then file specific client key. 23 24 ```text 25 $ ls unknown/firebase*
+18
content/blog/2024-12-16_airbuds.md
···
··· 1 + +++ 2 + title = "Airbuds" 3 + date = 2024-12-16 4 + slug = "airbuds" 5 + description = "Trying to break their api." 6 + draft = true 7 + 8 + [taxonomies] 9 + tags = ["reverse engineering", "graphql"] 10 + +++ 11 + 12 + Recently my cousin introduced me to the [Airbuds](https://airbuds.fm) app. Naturally I used it for a little bit. Slept a bit. And then booted up Proxypin to see if I could extract phone numbers from the app. With the base requests it appeared that I couldn't (:sadge:). I could get my phone number for my own profile however so I knew that it was likely stored in a user record somewhere (editor kieran: *umm yeah duh*). The more interesting part of this though was that it was a graphql api. 13 + 14 + <!-- more --> 15 + 16 + ## Phase 2 17 + 18 + Now knowing that it had a graphql api I wanted to see if there was a way to reverse engineer it. I have had suprisingly little experience with them but doing some quick ducking revealed that they can potentially have introspection enabled allowing us to get a full schema of what we can get. That sounds awesome but hopefully from a security standpoint unlikely to be enabled.
+28
content/blog/2025-01-01_spotify-to-apple-music.md
···
··· 1 + +++ 2 + title = "Exodus of Spotify Songs to the land of Apple Music" 3 + date = 2025-01-01 4 + slug = "spotify-to-apple-music" 5 + description = "Homegrown solution rather than paying for it ^-^" 6 + draft = true 7 + 8 + [taxonomies] 9 + tags = ["apple", "music"] 10 + +++ 11 + 12 + Today my family decided to get an Apple One subscription and use Apple Music instead of spotify. It makes sense from a cost standpoint (spotify is $20 a month vs $37.95 and `2TB` of storage plus all apple subscriptions) but I have about 3 years of history on spotify (1267 at time of writing) so manually transferring the songs isn't an option. I did some research but all I found was over priced apps and annoying python scripts. 13 + 14 + <!-- more --> 15 + 16 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/f17f56ea1780c37519a4f2cc5d866124acfe476e_0img_3821.jpg" alt="screenshot of the apple music app saying welcome to apple music" caption="the proper horror this should/does instill ๐Ÿ’€") }} 17 + 18 + ## Shortcut Time 19 + 20 + I haven't played around with apple shortcuts near enough but I know that they can be quite powerful (case in point [eieio.games](https://eieio.games/blog/doom-in-the-ios-photos-app/)). I looked to see whether spotify had a shortcut to get songs out first but didn't find anything (come on spotify!) but then when I checked Apple Music it expectedly had quite a few options. One of the options is add to playlist which when I tested it initially with the share sheet as input could take a spotify url. That got me thinking; why can't I just import a file of urls on new lines? Turns out that's exactly what you can do. If you start with a file as the input and then bring it to a split text block then you can route that directly to the add songs block! Whats even better is that you don't even need some fancy looping system, you can simply dump thousands of songs into it and it takes care of it super easily. 21 + 22 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/b05061099d67aa2297e991a074dd6e95bd33096d_0img_3824.png" alt="screenshot of the shortcut" caption="if you want to try it yourself you could build the shortcut from scratch or you can use the link below") }} 23 + 24 + Now the second part of the puzzle was exporting the liked playlist. I really didn't want to mess with the slack api and registering an oauth app but then I remembered that you can simple just hit control + a to select songs in the desktop app ๐Ÿคฆ and turns out if you copy it then it literally just chucks it all into your clipboard as spotify links on newlines. A quick `vi test.txt` and sending the file to myself over slack latter I could simply select the song file and use the share sheet to import it. It took a solid 35 seconds to import but gave a nice progress bar up top! 25 + 26 + ### Adendum 27 + 28 + - [the apple shortcut] for your copy pasta pleasure
+37
content/blog/2025-01-31_my-life-story-with-tech.md
···
··· 1 + +++ 2 + title = "My life story in tech so far ig ๐Ÿคท" 3 + date = 2025-01-31 4 + slug = "my-life-story-with-tech" 5 + description = "I was applying for a college cybersecurity camp and wrote this absolute monster that amounts to an overview of my life in tech so far (till 16)" 6 + 7 + [taxonomies] 8 + tags = ["yap fest", "biography"] 9 + +++ 10 + 11 + I was applying for a Cybersecurity college camp for this summer and realized this is honestly a pretty good summary of my life in tech so far (till i'm 16) and that I should probably make it a blog post soooo here it is! 12 + 13 + <!-- more --> 14 + 15 + ## The yap 16 + 17 + Hi! My name is Kieran, and I've been interested in / involved with cybersecurity and programming since I first started using a laptop at 10! I started out with a raspberry pi 3b+ which taught me how to use debian as well as the basics of creating and maintaining databases and web services. I moved on to an ubuntu laptop about a year latter and started using my raspberry pi as a home server to run small websites on our local lan. Soon I wanted to share them with others and expose them to the internet, so I learned how to use dns and port forwarding and then how to secure the server to prevent attacks with tools like fail2ban! 18 + 19 + ![2 boxes of electronics sitting on a closet shelf](https://l4.dunkirk.sh/i/_nNZW8nDrtsX.webp){caption="I still have that same rpi today! It's joined with all the random tech bits in two enormously heavy bins in my closet"} 20 + 21 + Over the next 2 years, I systematically read every single book in the tech section of my local library and became interested in white-hat hacking. I taught myself how to use kali linux and metasploit with the help of many web searches and had quite a bit of fun rooting and then sideloading custom payloads onto our families set of kindle fires (I was eventually restricted to just playing with just one but I did make a home security system with all of them once). I figured out wireshark and started playing with wifi protocals but eventually reached the limit of what I could figure out on my own and took a quick detour of two years to learn blender and build my first computer. 22 + 23 + I became interested in home labs and self hosting services around 14 and bought an old workstation off ebay which combined with my set of 3 rasberry pis and several old laptops (and one old pentium tower that I found on the side of the road) made quite a nice playground for deploying my own services. Half a year later I had to pick it all up and move up north which was quite the adventure; my services got completely messed up during the move, and it took my a week or so tinkering with everything to get it back to a stable state. 24 + 25 + ![gif of my github contributions graph 2021-2025](https://hc-cdn.hel1.your-objectstorage.com/s/v3/bf06e9d57d41dd75e328b8898cfe04ef2f30a3f3_0contributions-graph.gif){caption="2021-2022 is mainly just unity and hugo sites lol; I really started seriously using it and doing contributions to other projects 2023-2025. You can also see where I broke my wrist in January of 2025"} 26 + 27 + After the move, I became quite interested in front end development and started making quite a few websites and various random coding projects. If you look on my GitHub contributions graph ([github.com/taciturnaxolotl](https://github.com/taciturnaxolotl), you can see it go from a lightly speckled grid in 2021 and 2022 to a much more solid commit streak in 2023. I only had one week when I didn't code anything and that was the second week of the year :) Toward the end of that year I started learning about hardware design and made my first PCB! I also joined a wonderful community called hackclub where I met a ton of amazing teenagers who were also interested in tech just like me! I joined an FRC robotics team in January of the next year and had a blast designing, building, and programming a custom meter square, 150 lb, industrial robot to compete in that year's game! 28 + 29 + ![purple bubble logo](https://l4.dunkirk.sh/i/nvbGwaMylcQ2.webp){caption="I loved working on purple bubble ๐Ÿ’– i worked with some pretty incredible people and learned a ton. ik know yall are probably reading this when rss drops it so ๐Ÿซถ"} 30 + 31 + During that same time I also started a 501(c)3 named Purple Bubble with friends that I had met through Hackclub focused on making a secure, cost-effective, and privacy preserving messaging protocol. We drafted a specification and poured many, many hours into planning and developing the protocol over the next year but eventual realized that the messaging protocol space is *incredibly* hard and that there were innate flaws in our protocol that would compromise the security of the app (We couldn't find a good way to anonymize connections to a network of server's while also providing zero metadata transfer of messages between servers; we had originally planned for the protocol to be zero trust federated, but this proved to be a challenge that, no matter how hard we kept thinking and talking about it, we couldn't find a solution too). I learned a huge amount about organizing a group of people and running an organization through that experience and made some wonderful friends, so it wasn't entirely in vain. 32 + 33 + My latest project and biggest learning experience in both security and development has been building a time tracking server for coding called Hackatime. It is fully compatible with the popular wakatime.org, which allows it to leverage the hundreds of existing extensions for tracking time spent coding in almost every popular IDE and editor. I made this as a part of an event Hackclub ran called High Seas where they encouraged high school students to make cool projects by giving out awesome prizes for time spent coding (you had to "ship" your project where it would get voted on by the other four thousand teens participating and then via a custom ELO system convert your hours into "doubloons" that could be redeemed for prizes like framework laptops, soldering irons, McMaster Car credits, and many others. If you want to learn more about it, the website is [highseas.hackclub.com](https://highseas.hackclub.com)). In order to track the time of the thousands of teenagers participating, I created this server which was handling thousands of users an hour and hundreds of requests a second. I learned how to scale the server and database and learned an incredible amount that only comes at scale. At one point I got an email that the database bill had increased so much over the previous month that we were going to hit both the `$1k` hard limit and then a `$4k` limit that I had placed on the monthly bill, expecting never to hit it. The team hosting the database (Cockroach DB) graciously offered to reduce our bill down to only `$500` which was incredible. There were many more instances where things broke, or where I discovered security issues that made me grow an insane amount in my knowledge of how to fix things and really pushed me out of my comfort zone. (If you want to take a look at the github repo it is at [github.com/hackclub/hackatime](https://github.com/hackclub/hackatime) and the hosted version is at [waka.hackclub.com](https;//waka.hackclub.com) with a live hours counted tracker) 34 + 35 + ![the cockroach charges in hcb](https://l4.dunkirk.sh/i/QifqA0Ob4VJZ.webp){caption="The price really sky rocketed as we started using it in prod ๐Ÿ˜‚"} 36 + 37 + I'm still trying to figure out what exactly I want to major in, and I'm pretty solidly split between Comp Sci with a cybersecurity focus and Computer/Electrical Engineering. I'm hoping that this camp can help make that decision a bit more clear and give me a better understanding of what getting a major in Cyber Security would be like!
+120
content/blog/2025-02-02_degraded-zpool-proxmox.md
···
··· 1 + +++ 2 + title = "Fixing a degraded zpool on proxmox" 3 + date = 2025-02-03T10:00:00 4 + slug = "degraded-zpool-proxmox" 5 + description = "replacing a failed drive in a proxmox zpool" 6 + 7 + [taxonomies] 8 + tags = ["homelab", "tutorial"] 9 + +++ 10 + 11 + I decided to finally fix the network issues with my proxmox server (old static ip and used vlans which I hadn't setup with the new switch and router) as I had some time today but after fixing that fairly easily I discovered that my main `2.23 TB` zpool had a drive failure. Thankfully I had managed to stuff 3 disks into the case before so loosing one meant no data loss (thankfully ๐Ÿ˜ฌ; all my projects from the last 5 years as well as my entire video archive is on this pool). I still have 3 more disks of the same type so I can swap in a new one 2 more times after this. 12 + 13 + <!-- more --> 14 + 15 + ![the zpool reporting a downed disk](https://l4.dunkirk.sh/i/tN1RfSRLAeo2.webp){caption="That really scared the pants off me when I first saw it ๐Ÿ˜‚"} 16 + 17 + ## Actually fixing it 18 + 19 + First I had to find the affected disk physically in my case. Because I was stupid I didn't bother to label them but thankfully the serial numbers of the drives are stuck to them with a sticker so that wasn't terrible. 20 + 21 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/a6512def9bbeedbc1315a8ee58c92fbfb9e4d169_0image_from_ios.jpg" alt="chick-fil-a macaroni and cheese with 2 nuggets and some ketchup" caption="(By this point I had spent 30 minutes moaning so I went to lunch)") }} 22 + 23 + Now we can run `lsblk -o +MODEL,SERIAL` to find the serial number of our new drive. 24 + 25 + > root@thespia:~# lsblk -o +MODEL,SERIAL 26 + ```bash 27 + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS MODEL SERIAL 28 + sda 8:0 0 698.6G 0 disk ST3750640NS 3QD0BG0J 29 + โ”œโ”€sda1 8:1 0 698.6G 0 part 30 + โ””โ”€sda9 8:9 0 8M 0 part 31 + sdb 8:16 0 698.6G 0 disk ST3750640NS 3QD0BN6V 32 + sdc 8:32 0 698.6G 0 disk ST3750640NS 3QD0BQ5G 33 + โ”œโ”€sdc1 8:33 0 698.6G 0 part 34 + โ””โ”€sdc9 8:41 0 8M 0 part 35 + sdd 8:48 1 111.8G 0 disk Hitachi HTS543212L9SA02 090130FBEB00LGGJ35RF 36 + โ”œโ”€sdd1 8:49 1 1007K 0 part 37 + โ”œโ”€sdd2 8:50 1 512M 0 part /boot/efi 38 + โ””โ”€sdd3 8:51 1 111.3G 0 part 39 + โ”œโ”€pve-swap 253:0 0 8G 0 lvm [SWAP] 40 + โ”œโ”€pve-root 253:1 0 37.8G 0 lvm / 41 + โ”œโ”€pve-data_tmeta 253:2 0 1G 0 lvm 42 + โ”‚ โ””โ”€pve-data-tpool 253:4 0 49.6G 0 lvm 43 + โ”‚ โ”œโ”€pve-data 253:5 0 49.6G 1 lvm 44 + โ”‚ โ”œโ”€pve-vm--100--cloudinit 45 + โ”‚ โ”‚ 253:6 0 4M 0 lvm 46 + โ”‚ โ”œโ”€pve-vm--101--cloudinit 47 + โ”‚ โ”‚ 253:7 0 4M 0 lvm 48 + โ”‚ โ”œโ”€pve-vm--103--disk--0 49 + โ”‚ โ”‚ 253:8 0 4M 0 lvm 50 + โ”‚ โ””โ”€pve-vm--103--disk--1 51 + โ”‚ 253:9 0 32G 0 lvm 52 + โ””โ”€pve-data_tdata 253:3 0 49.6G 0 lvm 53 + โ””โ”€pve-data-tpool 253:4 0 49.6G 0 lvm 54 + โ”œโ”€pve-data 253:5 0 49.6G 1 lvm 55 + โ”œโ”€pve-vm--100--cloudinit 56 + โ”‚ 253:6 0 4M 0 lvm 57 + โ”œโ”€pve-vm--101--cloudinit 58 + โ”‚ 253:7 0 4M 0 lvm 59 + โ”œโ”€pve-vm--103--disk--0 60 + โ”‚ 253:8 0 4M 0 lvm 61 + โ””โ”€pve-vm--103--disk--1 62 + 253:9 0 32G 0 lvm 63 + sde 8:64 0 465.8G 0 disk WDC WD5000AAKS-65YGA0 WD-WCAS83511331 64 + โ”œโ”€sde1 8:65 0 465.8G 0 part 65 + โ””โ”€sde9 8:73 0 8M 0 part 66 + sdf 8:80 1 0B 0 disk Multi-Card 20120926571200000 67 + zd0 230:0 0 32G 0 disk 68 + โ”œโ”€zd0p1 230:1 0 100M 0 part 69 + โ”œโ”€zd0p2 230:2 0 16M 0 part 70 + โ”œโ”€zd0p3 230:3 0 31.4G 0 part 71 + โ””โ”€zd0p4 230:4 0 522M 0 part 72 + zd16 230:16 0 80G 0 disk 73 + โ”œโ”€zd16p1 230:17 0 1M 0 part 74 + โ””โ”€zd16p2 230:18 0 80G 0 part 75 + zd32 230:32 0 4M 0 disk 76 + zd48 230:48 0 80G 0 disk 77 + โ”œโ”€zd48p1 230:49 0 1M 0 part 78 + โ””โ”€zd48p2 230:50 0 80G 0 part 79 + zd64 230:64 0 32G 0 disk 80 + โ”œโ”€zd64p1 230:65 0 512K 0 part 81 + โ””โ”€zd64p2 230:66 0 32G 0 part 82 + zd80 230:80 0 1M 0 disk 83 + ``` 84 + 85 + Our two current drives are `3QD0BG0J` and `3QD0BQ5G` as we can see in proxmox but we can also see that they have partitions and `sdb/3QD0BN6V` does not so thats our target drive. Now we can find the disk by id with `ls /dev/disk/by-id | grep 3QD0BN6V` which gives us: 86 + 87 + > ls /dev/disk/by-id | grep 3QD0BN6V 88 + ```bash 89 + ata-ST3750640NS_3QD0BN6V 90 + ``` 91 + 92 + ![chick-fil-a macaroni and cheese with 2 nuggets and some ketchup](https://l4.dunkirk.sh/i/9tkdGhqIYmxt.webp){caption="My case situation is a bit of a mess and I'm using old 7200rpm server drives for pretty much everything; the dream is a 3 drive 2 TB each m.2 nvme ssd setup, maybe someday ๐Ÿคท"} 93 + 94 + We are going to go with the first id so no we move on to the zfs part. Running `zpool status vault-of-the-eldunari` we can get the status of the pool: 95 + 96 + > zpool status vault-of-the-eldunari 97 + ```bash 98 + pool: vault-of-the-eldunari 99 + state: DEGRADED 100 + status: One or more devices could not be used because the label is missing or 101 + invalid. Sufficient replicas exist for the pool to continue 102 + functioning in a degraded state. 103 + action: Replace the device using 'zpool replace'. 104 + see: https://openzfs.github.io/openzfs-docs/msg/ZFS-8000-4J 105 + scan: resilvered 8.33G in 00:48:26 with 0 errors on Thu Nov 14 18:38:03 2024 106 + config: 107 + 108 + NAME STATE READ WRITE CKSUM 109 + vault-of-the-eldunari DEGRADED 0 0 0 110 + raidz1-0 DEGRADED 0 0 0 111 + 9201394420428878514 UNAVAIL 0 0 0 was /dev/disk/by-id/ata-ST3750640NS_3QD0BM29-part1 112 + ata-ST3750640NS_3QD0BQ5G ONLINE 0 0 0 113 + ata-ST3750640NS_3QD0BG0J ONLINE 0 0 0 114 + 115 + errors: No known data errors 116 + ``` 117 + 118 + We can add our new disk with `zpool replace vault-of-the-eldunari 9201394420428878514 ata-ST3750640NS_3QD0BN6V` but first we wipe the disk from proxmox under the disks tab on our proxmox node to make sure its all clean before we restore the pool after we do that we also initalize a new gpt table. Now we are ready to replace the disk. Running this command can take quite a while and it doesn't output anything so sit tight. After waiting a few minutes proxmox reported that resilvering would take 1:49 minutes and it was 5% done already! I hope this helped at least one other person but I'm mainly writing this to remind myself how to do this when it inevitably happens again :) 119 + 120 + ![the zpool reporting a downed disk](https://l4.dunkirk.sh/i/2fbRM2bxdHKc.webp){caption="It's slow but faster then I expected for HDDs"}
+50
content/blog/2025-02-15_remove-exif-git-hook.md
···
··· 1 + +++ 2 + title = "Cleaning exif data with git pre-commit" 3 + date = 2025-02-15T19:57:01 4 + slug = "remove-exif-git-hook" 5 + description = "took longer then it probably should have ๐Ÿ˜Š" 6 + 7 + [taxonomies] 8 + tags = ["mildrant", "tutorial"] 9 + +++ 10 + 11 + I saw this [post](https://jade.fyi/blog/pre-commit-exif-safety/) from [jade.fyi](https://jade.fyi) on using a git hook to clear exif data from your images before you commit them and realized I should probably implement that too lol. Interestingly jade also uses zola for her site but she used pre-commit hooks whereas I wanted to do something that used native git hooks. 12 + 13 + <!-- more --> 14 + 15 + I started with the naive method of just having a `.git/hooks/pre-commit` file that would run `exiftool` on the input but after realizing that hooks placed there wouldn't be synced to the repo decided that wasn't the best way. I moved to using a script that would symlink files from the `hooks` directory to `.git/hooks`. It worked moderately well but due to the fact that I used (yes I feel the shame admitting this [:uw_embarrassed:](https://cachet.dunkirk.sh/emojis/uw_embarrassed/r)) `#!/bin/bash` instead of `#!/usr/bin/env bash`. Not realizing my mistake and believing it to be related to the symlink I found [this stack overflow](https://stackoverflow.com/questions/4592838/symbolic-link-to-a-hook-in-git/#:~:text=While%20you%20can%20use%20symbolic%20links) answer which taught me that you can use `git config core.hooksPath hooks` to move the hooks directory to `./hooks` in the root of your repo! After doing that and it still not working (i feel very dense writing this lol) I finally realized that the shebang was wrong and then it worked! 16 + 17 + ![the commit hook finally working!](https://l4.dunkirk.sh/i/Q-zmdBNx9Bee.webp){caption="phew"} 18 + 19 + Is there anything at all to learn from this? Well yes actually! You can use the script below and the `git config core.hooksPath hooks` setting to scrub your own images! 20 + 21 + > hooks/pre-commit 22 + ```bash 23 + #!/usr/bin/env bash 24 + 25 + # Check if exiftool is installed 26 + if ! command -v exiftool &> /dev/null; then 27 + echo "Error: exiftool is not installed. Please install it." >&2 28 + exit 1 29 + fi 30 + 31 + while read -r file; do 32 + case "$file" in 33 + *.jpg|*.jpeg|*.png|*.gif|*.tiff|*.bmp) 34 + echo "Removing EXIF data from: $file" >&2 35 + exiftool -all= --icc_profile:all -tagsfromfile @ -orientation -overwrite_original "$file" 36 + if [ $? -ne 0 ]; then 37 + echo "Error: exiftool failed to process $file" >&2 38 + exit 1 39 + fi 40 + git add "$file" 41 + ;; 42 + *) 43 + ;; 44 + esac 45 + done < <(git diff --cached --name-only --diff-filter=ACMR) 46 + 47 + exit -0 48 + ``` 49 + 50 + > if you want to add something or comment on the post then I posted about it on bluesky: [https://bsky.app/profile/dunkirk.sh/post/3liaybkkas226](https://bsky.app/profile/dunkirk.sh/post/3liaybkkas226)
+146
content/blog/2025-02-26_spherical-ray-diagrams.md
···
··· 1 + +++ 2 + title = "Determining the properties of a spherical mirror with ray diagrams" 3 + date = 2025-02-26 4 + slug = "spherical-ray-diagrams" 5 + description = "yes i made a tool to help with it :)" 6 + 7 + [taxonomies] 8 + tags = ["tool", "fancy", "physics"] 9 + +++ 10 + 11 + I was recently working through the Geometric Optics section of my physics textbook and was having trouble drawing all the ray diagrams (my wrist is still in a cast though that should come off in a few weeks) so I decided to try and make a tool to make them for me instead! I rather expected this to be a fairly simple process but instead it ended up being one of the most math intensive, most difficult โ€” and also most rewarding โ€” projects I've made recently! 12 + 13 + <!-- more --> 14 + 15 + ## the tool (๐Ÿฅ roll please) 16 + 17 + > this tool does support keyboard navigation btw ^-^ 18 + > `arrow keys` to move and `+` and `-` to zoom 19 + 20 + {{ lensDiagram() }} 21 + 22 + ## the math 23 + 24 + I was able to make it a bit simpler by restricting the domain of this tool to spherical mirrors (the only type used in this Module of my physics textbook) but I did tackle both concave and convex mirrors. It generates 3 rays: a horizontal ray, a ray through the focal point, and a ray through the radius of curvature. The first and last are quite easy to generate but the third was a bit more difficult. I ended up using a formula that I don't quite understand to get the point on the mirror where the ray intersects but it does work so ๐Ÿคท. 25 + 26 + The horizontal ray was dead simple. Draw a line from the top of the arrow to the edge of the mirror and then draw another line from focal point through the intersection point in the mirror. The part of that ray that is behind the mirror is simply the extension of the ray for virtual images but the part in front of the mirror is the actual path of the ray. 27 + 28 + ```javascript 29 + // Draw the horizontal ray 30 + ctx.strokeStyle = "green"; 31 + ctx.beginPath(); 32 + ctx.lineTo(objX, objY - h); 33 + let intersectionX = 34 + Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX; 35 + ctx.lineTo(intersectionX, objY - h); 36 + extendRayToCanvasEdge( 37 + intersectionX, 38 + objY - h, 39 + centerX - F * scale, 40 + centerY, 41 + ); 42 + ctx.stroke(); 43 + ``` 44 + 45 + The ray through the radius of curvature was also fairly simple but alot more fun to figure out the math for. Since we know that there is a right angle triange between the arrow, center line, and the radius we can use the pythagorean theorem to find the missing side of the intersection height and then we can use the ratio of the radius to the arrow base to find the proper x offset. 46 + 47 + ```javascript 48 + // Draw the ray through the radius of curvature 49 + ctx.strokeStyle = "orange"; 50 + ctx.beginPath(); 51 + ctx.lineTo(objX, objY - h); 52 + ctx.lineTo(circleCenterX, centerY); 53 + const extendedRay3 = findCircleIntersection( 54 + R * scale, 55 + objX, 56 + h, 57 + circleCenterX, 58 + centerY, 59 + circleCenterX, 60 + centerY, 61 + ); 62 + ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y); 63 + extendRayToCanvasEdge( 64 + extendedRay3[0].x, 65 + extendedRay3[0].y, 66 + centerX - R * scale, 67 + centerY, 68 + ); 69 + ctx.stroke(); 70 + ``` 71 + 72 + The last ray, the one through the focal point, was the most difficult to figure out. I had to do quite a bit of geometry to find where this ray intersects the mirror. To find this intersection point I used a method that finds where a line intersects with a circle by solving a quadratic equation. This was necessary because the mirror is actually just part of a circle, and by finding where the ray intersects with that circle I can then determine if that intersection point is actually on the mirror's surface. 73 + 74 + ```javascript 75 + // Draw the ray through the focal point 76 + ctx.strokeStyle = "purple"; 77 + ctx.beginPath(); 78 + ctx.lineTo(objX, objY - h); 79 + ctx.lineTo(centerX - F * scale, centerY); 80 + const extendedRay2 = findCircleIntersection( 81 + R * scale, 82 + objX, 83 + h, 84 + centerX - F * scale, 85 + centerY, 86 + circleCenterX, 87 + centerY, 88 + ); 89 + ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y); 90 + ctx.lineTo(0, extendedRay2[0].y); 91 + ctx.stroke(); 92 + ``` 93 + 94 + The method works by taking the equation of the line between our arrow tip and focal point (y = mx + b) and the equation of our mirror's circle ((x-h)ยฒ + (y-k)ยฒ = rยฒ) and substituting one into the other. This gives us a quadratic equation that we can solve to find the x coordinates of the intersection points. Once we have these x values, we can plug them back into our line equation to get the y coordinates. 95 + 96 + Then we just need to check which of these intersection points is actually on the mirror's surface (since a line can intersect a circle in up to two points) and use that for our ray. From there, we can draw the reflected ray just like with the other two methods. 97 + 98 + I will freely admit that I made heavy use of gpt-4o to figure out the inital equations as thats a bit beyond the current scope of my knowledge. The rest of the ray logic was too complex for gemini or claude to figure out so that bit was all me ๐Ÿ˜Ž 99 + 100 + ```javascript 101 + // fancy complex scary math ๐Ÿ‘ป 102 + function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) { 103 + // Check if the input values are valid 104 + if (radius <= 0) { 105 + throw new Error("Invalid input values."); 106 + } 107 + 108 + // Calculate the slope of the line from (x1, h) to (x3, y3) 109 + const m = (y3 - (centerY - h)) / (x3 - x1); 110 + 111 + // Define the line equation: y = h + m * (x - x1) 112 + // Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2 113 + // y = h + m * (x - x1) 114 + // (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2 115 + 116 + // Coefficients for the quadratic equation 117 + const a = 1 + m * m; 118 + const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1); 119 + const c = 120 + centerX * centerX + 121 + (centerY - h - centerY - m * x1) * 122 + (centerY - h - centerY - m * x1) - 123 + radius * radius; 124 + 125 + // Calculate the discriminant 126 + const discriminant = b * b - 4 * a * c; 127 + 128 + if (discriminant < 0) { 129 + throw new Error("No intersection found."); 130 + } 131 + 132 + // Calculate the two possible x values 133 + const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a); 134 + const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a); 135 + 136 + // Calculate the corresponding y values 137 + const yIntersect1 = centerY - h + m * (xIntersect1 - x1); 138 + const yIntersect2 = centerY - h + m * (xIntersect2 - x1); 139 + 140 + // Return the intersection points 141 + return [ 142 + { x: xIntersect1, y: yIntersect1 }, 143 + { x: xIntersect2, y: yIntersect2 }, 144 + ]; 145 + } 146 + ```
+99
content/blog/2025-03-14_my-animations.md
···
··· 1 + +++ 2 + title = "All my animation projects" 3 + date = 2025-03-14 4 + slug = "my-animations" 5 + description = "finally collected in one place ๐ŸŽ‰" 6 + 7 + [taxonomies] 8 + tags = ["tool", "fancy", "physics"] 9 + +++ 10 + 11 + The other day I realized that I never made a page to collect all of my animation projects in one place. I've made quite a few of them over the years and untill now they have just been sitting in a giant folder on my nas. Now they are all in a nice clean collection here ๐ŸŽ‰ 12 + 13 + <!-- more --> 14 + 15 + ## 2021 16 + 17 + {{ youtube(id="O7SYcdUM8mI", caption="2021.01.27 first jelly jar") }} 18 + 19 + ![tesla in a showroom with fire jets](https://l4.dunkirk.sh/i/zfES0NVx7vcr.webp){caption="2021.02.10 tesla showroom"} 20 + 21 + {{ youtube(id="7Ozt7WcVwt0", caption="2021.09.27 Chalet a la Tagia minecraft animation") }} 22 + 23 + ![cube diorama](https://l4.dunkirk.sh/i/qa26wPWOhZUn.webp){caption="2021.12.15 cube diorama"} 24 + 25 + {{ youtube(id="O5iHoFwKQuE", caption="2021.12.17 creature walk cycle test") }} 26 + 27 + {{ youtube(id="Mh4BL8O6-i8", caption="2021.12.21 minecraft water vfx test") }} 28 + 29 + {{ youtube(id="xBn43UU_jak", caption="2021.12.23 fireball smoke / fire sim") }} 30 + 31 + ## 2022 32 + 33 + {{ youtube(id="R9SwANdkMf0", caption="2022.01.05 wave ball motion effects") }} 34 + 35 + {{ youtube(id="ru5QfeVqlUY", caption="2022.01.12 sunroom plant vfx") }} 36 + 37 + {{ youtube(id="bHqE4aHSMLU", caption="2022.01.20 star chase") }} 38 + 39 + {{ youtube(id="3JwTkVJ2WxU", caption="2022.01.21 star chase v2 in tunnel") }} 40 + 41 + {{ youtube(id="mNeEJ-VE0o8", caption="2022.01.21 particle path guides test") }} 42 + 43 + {{ youtube(id="Gy0K-Gi95Jg", caption="2022.01.22 lost music visualization") }} 44 + 45 + ![ice sphere](https://l4.dunkirk.sh/i/qrRxhAI_U04m.webp){caption="2022.01.24 ice icosphere"} 46 + 47 + ![glass jar with marbles](https://l4.dunkirk.sh/i/JQnWbotNPjCV.webp){caption="2022.01.27 marble jar"} 48 + 49 + {{ youtube(id="ue-hy7w1-JE", caption="2022.02.08 firefly particle sim") }} 50 + 51 + {{ youtube(id="JrzjPBDBPF0", caption="2022.02.09 color changing blocks vfx") }} 52 + 53 + {{ youtube(id="fh_cNR9QhdU", caption="2022.02.25 attempt at an epic chase scene") }} 54 + 55 + {{ youtube(id="BGJbmXqCD5M", caption="2022.03.16 molecular plexus") }} 56 + 57 + ![twisted torus with flattened sphere in the center](https://l4.dunkirk.sh/i/Lh-BLtahNCD3.webp){caption="2022.03.16 twisted torus"} 58 + 59 + {{ youtube(id="yT37oZmd4hc", caption="2022.03.17 hex tunnel") }} 60 + 61 + {{ youtube(id="3SQN0L0wbhU", caption="2022.03.23 wavy strips motion effects") }} 62 + 63 + ![airship far bottom](https://l4.dunkirk.sh/i/n4kJfXQbHpxp.webp){caption="2022.03.31 airship"} 64 + 65 + ![airship far side](https://l4.dunkirk.sh/i/MazhxSUOshdY.webp) 66 + 67 + ![airship front top](https://l4.dunkirk.sh/i/Qty_GYWgxJi6.webp) 68 + 69 + ![airship front cab](https://l4.dunkirk.sh/i/y8ROtoPXYWYP.webp) 70 + 71 + ![airship front side](https://l4.dunkirk.sh/i/Rr9jqcY8EPqn.webp) 72 + 73 + ![minecraft village front door with villager](https://l4.dunkirk.sh/i/KOOEOTt_6dDs.webp){caption="2022.04.06 viking village"} 74 + 75 + ![minecraft village from across a small lake with a skeleton and witch](https://l4.dunkirk.sh/i/DKf4tgKVpq6b.webp){caption="2022.04.06 mountain lake village"} 76 + 77 + {{ youtube(id="gPRrt_0NMKE", caption="2022.04.07 rolling balls motion effects") }} 78 + 79 + {{ youtube(id="hHIv2yO9DvU", caption="2022.04.29 cloth sim") }} 80 + 81 + {{ youtube(id="zqyv7GBTLGA", caption="2022.05.30 fire handwriting") }} 82 + 83 + ![a cylinder with a bunch of bumps on it](https://l4.dunkirk.sh/i/RB4ih_pdg3IW.webp){caption="2022.06.24 the cylinder"} 84 + 85 + {{ youtube(id="XVyMUROofZ8", caption="2022.07.21 the iconic donut") }} 86 + 87 + {{ youtube(id="ZGrNNnujR3o", caption="2022.07.25 Appletree SMP minecraft animation") }} 88 + 89 + {{ youtube(id="zRlgWbW1Qcw", caption="2022.08.01 mirror physics") }} 90 + 91 + ![the earth from space](https://l4.dunkirk.sh/i/oi58Ag23Pdmf.webp){caption="2022.08.30 the earth"} 92 + 93 + ![11 glowing pendulums swinging in a flowing curve](https://l4.dunkirk.sh/i/_s4ZYuQv9YLQ.webp){caption="2022.08.31 glowing pendulums"} 94 + 95 + ![an orange flower in a flower pot with skyline dirt](https://l4.dunkirk.sh/i/4LrJwRD-0bPo.webp){caption="2022.10.22 orange flower"} 96 + 97 + ## 2023 98 + 99 + {{ youtube(id="qnbGPErmmoI", caption="2023.11.07 FIRST Digital Animation Award submission") }}
+225
content/blog/2025-03-18_adding-a-copy-button.md
···
··· 1 + +++ 2 + title = "Adding a copy code button" 3 + date = 2025-03-14 4 + slug = "adding-a-copy-button" 5 + description = "continuing the chain :)" 6 + 7 + [taxonomies] 8 + tags = ["accessibility"] 9 + +++ 10 + 11 + It took me a little over a month but I finally continued the chain of adding copy code buttons to your code blocks. It started with Salma Alam-Naylorโ€™s [post](https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/) which I saw on Hacker News but then [David Bushell](https://dbushell.com/2025/02/14/copy-code-button/) also posted on it and [Ragman](https://www.ragman.net/musings/copy_code/) made a bluesky post (sky? bloop? atproto bloop? honestly not sure what a more interesting name would be) and it's been saved in my mind since then that I should add it. 12 + 13 + <!-- more --> 14 + 15 + What finally pushed me over the edge was seeing the [Duckquill](https://duckquill.daudix.one) theme and its fancy code blocks. I cloned the theme (`git clone https://codeberg.org/daudix/duckquill.git`) and figured out that the actual copy code was some reasonably simple js in `static/copy-button.js`. I copied that file and messed with it a bit as well as the css (`sass/_pre-container.scss` and some icon stuff in `sass/_icon.scss`) to make it work with my theme and style. 16 + 17 + A quick hash for cache busting and import later it all worked! 18 + 19 + > templates/head.html 20 + ```html 21 + {% set jsHash = get_hash(path="js/copy-button.js", sha_type=256, 22 + base64=true) %} 23 + <script 24 + src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}" 25 + defer 26 + ></script> 27 + ``` 28 + 29 + The one thing I expanded on was the ability to specify a file name / comment for the code block. When js is disabled a markdown `>` blockquote on the line before the code block will create a header tab for the code block. I snipped the header tab idea from [chevyray.dev](https://chevyray.dev) and I grew to quite like it so I didn't want to abandon it over a copy button. 30 + 31 + Here is my code should you want to use it: 32 + 33 + > static/js/copy-button.js 34 + ```js 35 + // Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html 36 + document.addEventListener("DOMContentLoaded", () => { 37 + const blocks = document.querySelectorAll("pre[class^='language-']"); 38 + 39 + for (const block of blocks) { 40 + if (navigator.clipboard) { 41 + // Code block header title 42 + const title = document.createElement("span"); 43 + const lang = block.getAttribute("data-lang"); 44 + const comment = 45 + block.previousElementSibling && 46 + (block.previousElementSibling.tagName === "blockquote" || 47 + block.previousElementSibling.nodeName === "BLOCKQUOTE") 48 + ? block.previousElementSibling 49 + : null; 50 + if (comment) block.previousElementSibling.remove(); 51 + title.innerHTML = 52 + lang + (comment ? ` (${comment.textContent.trim()})` : ""); 53 + 54 + // Copy button icon 55 + const icon = document.createElement("i"); 56 + icon.classList.add("icon"); 57 + 58 + // Copy button 59 + const button = document.createElement("button"); 60 + const copyCodeText = "Copy code"; // Use hardcoded text instead of getElementById 61 + button.setAttribute("title", copyCodeText); 62 + button.appendChild(icon); 63 + 64 + // Code block header 65 + const header = document.createElement("div"); 66 + header.classList.add("header"); 67 + header.appendChild(title); 68 + header.appendChild(button); 69 + 70 + // Container that holds header and the code block itself 71 + const container = document.createElement("div"); 72 + container.classList.add("pre-container"); 73 + container.appendChild(header); 74 + 75 + // Move code block into the container 76 + block.parentNode.insertBefore(container, block); 77 + container.appendChild(block); 78 + 79 + button.addEventListener("click", async () => { 80 + await copyCode(block, header, button); // Pass the button here 81 + }); 82 + } 83 + } 84 + 85 + async function copyCode(block, header, button) { 86 + const code = block.querySelector("code"); 87 + const text = code.innerText; 88 + 89 + await navigator.clipboard.writeText(text); 90 + 91 + header.classList.add("active"); 92 + button.setAttribute("disabled", true); 93 + 94 + header.addEventListener( 95 + "animationend", 96 + () => { 97 + header.classList.remove("active"); 98 + button.removeAttribute("disabled"); 99 + }, 100 + { once: true }, 101 + ); 102 + } 103 + }); 104 + ``` 105 + 106 + and the css: 107 + 108 + > sass/css/_copy-button.scss 109 + ```scss 110 + i.icon { 111 + display: inline-block; 112 + mask-size: cover; 113 + background-color: currentColor; 114 + width: 1rem; 115 + height: 1rem; 116 + font-style: normal; 117 + font-variant: normal; 118 + line-height: 0; 119 + text-rendering: auto; 120 + } 121 + 122 + .pre-container { 123 + --icon-copy: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' height='16' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 3c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3 0 .55-.45 1-1 1s-1-.45-1-1c0-.57-.43-1-1-1H3c-.57 0-1 .43-1 1v5c0 .57.43 1 1 1 .55 0 1 .45 1 1s-.45 1-1 1c-1.645 0-3-1.355-3-3zm5 5c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3v5c0 1.645-1.355 3-3 3H8c-1.645 0-3-1.355-3-3zm2 0v5c0 .57.43 1 1 1h5c.57 0 1-.43 1-1V8c0-.57-.43-1-1-1H8c-.57 0-1 .43-1 1m0 0'/%3E%3C/svg%3E"); 124 + --icon-done: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M7.883 0q-.486.008-.965.074a7.98 7.98 0 0 0-4.602 2.293 8.01 8.01 0 0 0-1.23 9.664 8.015 8.015 0 0 0 9.02 3.684 8 8 0 0 0 5.89-7.75 1 1 0 1 0-2 .008 5.986 5.986 0 0 1-4.418 5.816 5.996 5.996 0 0 1-6.762-2.766 5.99 5.99 0 0 1 .922-7.25 5.99 5.99 0 0 1 7.239-.984 1 1 0 0 0 1.363-.371c.273-.48.11-1.09-.371-1.367A8 8 0 0 0 9.492.14 8 8 0 0 0 7.882 0m7.15 1.998-.1.002a1 1 0 0 0-.687.34L7.95 9.535 5.707 7.29A1 1 0 0 0 4 8a1 1 0 0 0 .293.707l3 3c.195.195.465.3.742.293.277-.012.535-.133.719-.344l7-8A1 1 0 0 0 16 2.934a1 1 0 0 0-.34-.688 1 1 0 0 0-.627-.248'/%3E%3C/svg%3E"); 125 + 126 + margin: 1rem 0 1rem; 127 + border-radius: 0.75rem; 128 + 129 + .header { 130 + display: flex; 131 + justify-content: space-between; 132 + align-items: center; 133 + border-radius: 0.2em 0.2em 0 0; 134 + background-color: var(--accent); 135 + background-size: 200%; 136 + padding: 0.25rem; 137 + height: 2.5rem; 138 + 139 + span { 140 + margin-inline-start: 0.75rem; 141 + color: var(--purple-gray); 142 + font-weight: bold; 143 + line-height: 1; 144 + } 145 + 146 + button { 147 + appearance: none; 148 + transition: 200ms; 149 + cursor: pointer; 150 + border: none; 151 + border-radius: 0.4rem; 152 + background-color: transparent; 153 + padding: 0.5rem; 154 + color: var(--purple-gray); 155 + line-height: 0; 156 + 157 + &:hover { 158 + background-color: color-mix( 159 + in oklab, 160 + var(--accent) 80%, 161 + var(--purple-gray) 162 + ); 163 + } 164 + 165 + &:focus { 166 + background-color: color-mix( 167 + in oklab, 168 + var(--accent) 80%, 169 + var(--purple-gray) 170 + ); 171 + } 172 + 173 + &:active { 174 + transform: scale(0.9); 175 + } 176 + 177 + &:disabled { 178 + cursor: not-allowed; 179 + 180 + &:active { 181 + transform: none; 182 + } 183 + } 184 + 185 + .icon { 186 + -webkit-mask-image: var(--icon-copy); 187 + mask-image: var(--icon-copy); 188 + transition: 200ms; 189 + 190 + :root[dir*="rtl"] & { 191 + transform: scaleX(-1); 192 + } 193 + } 194 + } 195 + 196 + &.active { 197 + button { 198 + animation: active-copy 0.3s; 199 + 200 + color: var(--purple-gray); 201 + 202 + .icon { 203 + -webkit-mask-image: var(--icon-done); 204 + mask-image: var(--icon-done); 205 + } 206 + } 207 + 208 + @keyframes active-copy { 209 + 50% { 210 + transform: scale(0.9); 211 + } 212 + 100% { 213 + transform: none; 214 + } 215 + } 216 + } 217 + } 218 + 219 + pre { 220 + margin: 0; 221 + box-shadow: none; 222 + border-radius: 0 0 0.2em 0.2em; 223 + } 224 + } 225 + ```
+99
content/blog/2025-04-24_atuin.md
···
··· 1 + +++ 2 + title = "Musings about Atuin" 3 + date = 2025-04-24 4 + slug = "atuin" 5 + description = "its a bit tricky on nix, but it's sooo worth it" 6 + 7 + [taxonomies] 8 + tags = ["shell", "nix", "cool stuff"] 9 + +++ 10 + 11 + I've been on the fence about using [Atuin](https://atuin.sh) for about a month now. I heard about it from [Ellie](https://ellie.wtf) on bluesky and initially didn't bother setting it up since I didn't really care about whether my shell history was synced across devices as I'm only using one main device (framework 13 ๐Ÿ”ฅ) rn. I saw a repost of Ellie's about Atuin Desktop today and that finally pushed me over the edge to take the time to figure out how to get it setup with nix. 12 + 13 + <!-- more --> 14 + 15 + {{ bluesky(post="https://bsky.app/profile/ellie.wtf/post/3lng5ig2o722z") }} 16 + 17 + And it wasn't that hard! Atuin is published on nixpkgs or can be installed via a flake and there is a home manager module for it too! Once you get past actual installation and into using agenix to declaritively manage your secrets then it gets annoying (proly mainly because i'm still pretty stupid when it comes to nix lol). 18 + 19 + The first bit is that to access age secrets in home manager you have to actually export them in home manager (๐Ÿคฏ) and you can't just use the version from your `configuration.nix`. The second bit is that you **also** need to export the age file in your `configuration.nix` (that took me a solid half hour to figure out :uw_embarrassed:). The third and final thing however is that you can't just save the secret and key files with agenix like normal but you have to strip the line endings from them ๐Ÿ˜ญ. 20 + 21 + Here's the basic scaffolding you'll need for your Nix configuration: 22 + 23 + > configuration.nix 24 + 25 + ```nix 26 + { config, pkgs, ... }: 27 + 28 + { 29 + # ... configuration options 30 + 31 + age.secrets = { 32 + atuin-session = { 33 + file = ../secrets/atuin-session.age; 34 + mode = "0444"; 35 + }; 36 + atuin-key = { 37 + file = ../secrets/atuin-key.age; 38 + mode = "0444"; 39 + }; 40 + }; 41 + 42 + # ... more configuration options 43 + } 44 + ``` 45 + 46 + and then the home-manager bit 47 + 48 + > shell.nix in home manager 49 + 50 + ```nix 51 + { config, pkgs, ... }: 52 + 53 + { 54 + # ... some home-manager modules and configs 55 + 56 + programs.atuin = { 57 + enable = true; 58 + settings = { 59 + auto_sync = true; 60 + sync_frequency = "5m"; 61 + sync_address = "https://api.atuin.sh"; 62 + search_mode = "fuzzy"; 63 + session_path = config.age.secrets."atuin-session".path; 64 + key_path = config.age.secrets."atuin-key".path; 65 + }; 66 + }; 67 + 68 + age.secrets = { 69 + atuin-session = { 70 + file = ../../secrets/atuin-session.age; 71 + }; 72 + atuin-key = { 73 + file = ../../secrets/atuin-key.age; 74 + }; 75 + }; 76 + 77 + # ... even more home-manager configurations ๐Ÿ˜… 78 + } 79 + ``` 80 + 81 + Now saving the secrets with agenix is not particularly tricky you just have to know that this is an option. Run `agenix -e atuin-session.age` and then paste in the session from `~/.local/share/atuin/session` and then instead of just saving like normal you need to run `:set binary` and then `:set noeol` and then you can save the file like normal. 82 + 83 + Anyways now i'm enjoying my stats and it's on to the next project (proly [serif.blue](https://tangled.org/@dunkirk.sh/serif) ๐Ÿ‘€) 84 + 85 + ``` 86 + > atuin stats 87 + [โ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎโ–ฎ] 1209 gc 88 + [โ–ฎโ–ฎโ–ฎโ–ฎ ] 495 curl 89 + [โ–ฎโ–ฎ ] 348 bun 90 + [โ–ฎโ–ฎ ] 329 cat 91 + [โ–ฎ ] 222 z 92 + [โ–ฎ ] 200 g 93 + [โ–ฎ ] 162 nix-shell 94 + [โ–ฎ ] 145 cd 95 + [โ–ฎ ] 138 vi 96 + [โ–ฎ ] 138 ls 97 + Total commands: 7062 98 + Unique commands: 7060 99 + ```
+19
content/blog/2025-10-24_github-phishing.md
···
··· 1 + +++ 2 + title = "Novel phishing tactic using github notifications" 3 + date = 2025-10-24 4 + slug = "github-phishing" 5 + description = "the creators certainly didn't execute this very well" 6 + 7 + [taxonomies] 8 + tags = ["phishing"] 9 + +++ 10 + 11 + I received an email yesterday at `19:45 EST` titled `[yccombinator/-notification] Y-Combinator W2026 | $15M Y-Combinator & GitHub (Issue #126)`. From a quick glance it was easy to tell that it was a phising email funneling people to `https://y-comblnator.com/apply`. They did at least try to disguise the link but then there is a ton of whitespace and you can see that they tagged 32 github users including mine. 12 + 13 + <!-- more --> 14 + 15 + {{ img(id="https://hc-cdn.hel1.your-objectstorage.com/s/v3/47a842d35a86d6ac16d717b40ee69f2f801ff852_screenshot_2025-09-23_at_21.23.19.png" alt="a screenshot of the email" caption="I've never seen something simultaniously this stupid and (as far as i can tell) novel") }} 16 + 17 + Like most phishing emails I doubt most people would fall for this but if you were moving quickly and not thinking straight maybe you could fall for this? 18 + 19 + Cloudflare has blocked the site due to phishing by now (13:17 Sept 24th) which is a shame since I would have loved to dig into the site a bit.
+120
content/blog/2025-12-18_homelab-tour.md
···
··· 1 + +++ 2 + title = "Homelab tour" 3 + date = 2025-12-18 4 + slug = "homelab-tour" 5 + description = "what lives in my homelab in 2025" 6 + 7 + [taxonomies] 8 + tags = ["homelab"] 9 + +++ 10 + 11 + Well this is a post I somehow have procrastinated on. I originally got the idea to write this up for the [LUP holiday homelab special](https://linuxunplugged.com/646) but before I knew it the submission due date was upon me and I hadn't started so here goes a speedrun tour of my lab. 12 + 13 + <!-- more --> 14 + 15 + ![me with my trusty server](https://l4.dunkirk.sh/i/6PXIFV4xl2Ye.webp){caption="This is my server ember, say hi!"} 16 + 17 + I have a few main machines. In order of when I first got them they are: 18 + 19 + 1. broylt (~2021 main pc / gpu machine :windows:) 20 + 2. thespia (~2022 first homelab workstation; runs a vm named vault which houses my storage :proxmox:) 21 + 3. ember (~2024 first "real" server; dell poweredge r210 :ubuntu:) 22 + 4. nest (early 2024 shared tilde server @ Hackclub :nix:) 23 + 5. moonlark (late 2024 framework 13; was my main laptop but the mainboard fried itself and I haven't fixed it yet :nix:) 24 + 6. tacyon / pihole (summer 2025 rpi 5 home manager cyber pi / pihole :raspberry_pi:) 25 + 7. atalanta (late 2025 macbook air m4 16gb main computing device and what I'm typing this on rn :mac:) 26 + 8. terebithia (late 2025 oracle cloud free tier arm vm with 24gb ram and 150 gb of storage :nix:) 27 + 9. prattle (late 2025 oracle cloud amd vm that has since died but it used to be my monitoring node :nix:) 28 + 29 + As many of my machines as I can are running :nix: in some flavor. `moonlark` and `terebithia` are the only ones running nixos at the moment but I'm planning on switching `ember` over soon as well as maybe resurrecting `prattle` as well. All my other machines minus thespia and broylt are running my [dots](https://tangled.org/dunkirk.sh/dots) via home manager. 30 + 31 + ### Ember 32 + 33 + ![neofetch on ember](https://l4.dunkirk.sh/i/6z4tVvS9eGt2.webp) 34 + 35 + This used to run all of my services up till a month ago when I setup my oracle cloud instance and switched all of my main hosted services over. It currently runs my jellyfin / arr stack and any random workloads I want to throw somewhere without setting up a nix config for it. It in the very near future is probably going to become a quick deploy server in lieu of railway and similar services. The plan is to have a simple cli that will just throw everything on the server from a local repo and just run it with a domain. 36 + 37 + For jellyfin the best way I have found to interact with it is via `Ruddar` on my phone and `Infuse` on either my phone or apple tv. It works insanely well for streaming stuff and I'm very tempted to buy a life time sub but I just don't use my media library enough for it to be justified. 38 + 39 + ### Terebithia 40 + 41 + ![neofetch on terebithia](https://l4.dunkirk.sh/i/5qwQ7HP0nm4e.webp) 42 + 43 + This has really been a rock solid machine for me. I have it setup with my nix config and it auto deploys with `deploy-rs` from github actions over tailscale. It is running my [tangled.org](https://tangled.org/@dunkirk.sh) knot so it hosts all of my git repos and I have a fancy little automation that sets up git hooks in the repos as they are created to auto mirror to github. It is also running [cachet](https://cachet.dunkirk.sh) which is a slack profile picture and emoji proxy (thats where all the emojis in this blog are pulled from) that I built to work at extremely high scale. It was running on `ember` for a while but I swapped it over to here to be a bit more reliable since so many people in Hackclub rely on it now. I'm also running a few slack bots and [battleship-arena](https://battle.dunkirk.sh) again through their own nix services. 44 + 45 + ![bore screenshot](https://l4.dunkirk.sh/i/TTbQriehUJDb.webp){caption="bore in all it's glory"} 46 + 47 + My most exciting services hosted on here though are [bore](https://bore.dunkirk.sh) and [indiko](https://indiko.dunkirk.sh/docs). Bore is a little wrapper I made around [frp](https://github.com/fatedier/frp) which allows me to easily deploy and view my tunnels. It is essentially an ngrok replacement and super slick to use. I made a custom cli for it with `gum` and nix which you can find along with setup instructions on the [tangled repo](https://tangled.org/dunkirk.sh/dots/tree/main/modules/nixos/services/bore). 48 + 49 + !![oauth screenshot](https://l4.dunkirk.sh/i/K-GWKS8Lh-CQ.webp)[users managment](https://l4.dunkirk.sh/i/N8I51AOs-va7.webp){caption="the indiko admin ui and oauth consent screen"} 50 + 51 + My newest project is [Indiko](https://tangled.org/dunkirk.sh/indiko/). It is a selfhosted IndieAuth / OAuth 2.0 compatible auth server somewhat like Authelia or Authentik but mine! I can defined custom clients and then use it to authorize with my own apps. I'm planning to add support to bore for using indiko as an authentication middleware on protected tunnels probably tomorrow. I'm currently using this to authenticate my shortlinks service [hop](https://hop.dunkirk.sh) ([repo](https://tangled.org/dunkirk.sh/hop)) which allows me to add new viewers and admins on the fly! 52 + 53 + There is still a ton of head room on this server so I'm looking forward to adding quite a bit more here. 54 + 55 + #### The nix services stack 56 + 57 + Right now I have a pretty sweet stack for how I deploy my apps with nix. I have a caddy setup that is connected to my cloudflare account for auto generating certifications via dns attestation and then because it's nix I can just add a new caddy block and it adds it onto my config file. 58 + 59 + The way it works is I have service modules in `modules/nixos/services/` that follow a pretty consistent pattern. 60 + Each one has options for the domain, port, and secrets file. When you enable a service it creates a system user, sets up passwordless sudo for restarting the systemd service (needed for non interactive CI/CD), and then runs the app with whatever command is needed. The cool part is in the preStart where it git clones the repo if it doesn't exist and then optionally pulls on restart so I can just push to github and restart the service to deploy. Most of the time though I set up a workflow with tailscale to ssh in and pull down the new content and then restart. I have finally started using proper acls and tags so I feel very proud of myself. 61 + 62 + Each service automatically configures its own caddy virtualHost with cloudflare dns challenge for TLS. So when I add a 63 + new service I just do: 64 + 65 + ```nix 66 + { 67 + atelier.services.cachet = { 68 + enable = true; 69 + domain = "cachet.dunkirk.sh"; 70 + secretsFile = config.age.secrets.cachet.path; 71 + }; 72 + } 73 + ``` 74 + 75 + And boom - it clones the repo, installs dependencies, starts the systemd service, and sets up caddy with automatic 76 + HTTPS. All the secrets are managed with agenix so they're encrypted with my ssh key and decrypted at boot. The 77 + cloudflare API token gets injected into caddy's environment so the DNS challenge just works. 78 + 79 + For more complex services like indiko I can add rate limiting in the caddy config -- I have different rate 80 + limits for `/auth/_`, `/api/_`, and general routes all configured in the service module itself. It's all declarative 81 + so the entire stack (caddy, TLS, DNS, users, app deployment) is just nix config that can be auto deployed via deploy-rs from 82 + github actions over tailscale. 83 + 84 + ### Thespia 85 + 86 + !![the harddrives falling out of the case](https://l4.dunkirk.sh/i/PRp924X_rorV.webp)[harddrives in the front](https://l4.dunkirk.sh/i/tBkhMjYzfH-K.webp){caption="I have been blessed with ~5tb of HDD storage but I currently have no place to put it so this is only about 1.5 TB and it is truely attrocious"} 87 + 88 + This is the most jank part of the whole lab. It is one of my oldest dedicated machines and it is kind of showing it's age. I really don't do much with it anymore but the harddrives are still somehow kicking. I inherited a large cardboard box of drives from my great uncle (I also got ember from him) and they all have about 10-15% of life left but are in 500 GB and 750 GB sizes which is a wee bit annoying. I'm most likely going to get a beelink or something else that is small and power efficent and just shove a ridiculous amount of m.2 SSDs in there. 89 + 90 + This used to my my pride and joy and I paired it with an old laptop (laptops make shockingly good budget servers btw; they have essentially a built in ups after all and are quite power efficent at times) which was named `thalia`. Between those two I made a multi-node proxmox cluster and it was amazing. 91 + 92 + ### Broylt 93 + 94 + This is my first PC built from back when I was 13. I got some of the parts for my birthday and saved up to buy the rest and it was glorious when I first built it. Honestly it hasn't changed a ton since back then. I swapped out the power supply as the original one kept failing and I had to RMA it. I also finally swapped the GPU to a 2070 Super a year or two ago which has been amazing. It is still rocking the original SSD, Asus B550M Plus Wifi motherboard, and the ryzen 5 3600 CPU and honestly I have very little complaints expect for the fact it is windows and that it keeps randomly crashing if I do anything too intense with the GPU. Honestly the issue is probably just that it needs a bigger PSU and as for windows thats really the only reason I still keep it around as you really still do need a windows box for running odd jobs and perhaps most importantly the occasional valorant session. 95 + 96 + ### Atalanta 97 + 98 + This is my macbook which I got this fall after my framework died. It definitly is a wee bit more reliable than my hacky hyprland config but I do still miss my framework. I will probably end up reviving it soon with a new mainboard and it will become my dual boot nixos / windows competition laptop for both FRC (robotics) and various cyber competitions. 99 + 100 + ![shell config](https://vhs.charm.sh/vhs-1KyplPxIt9f1kNmruDVPVS.gif) 101 + 102 + As far as special stuff on this machine there isn't a ton to tell. I'm doing some fancy configuration with nix darwin to make the default screenshot action copy to clipboard and I'm completely removing all pinned items from the doc but I really could be doing more (thanks to [Nick Welsh](https://github.com/nickwelsh/nix-darwin-config) for helping me realize this was possible last config confessions on LUP). I do have my custom shell config which I have been refining for the better part of a year and a half now. It was originally inspired by the [Dreams of Code](https://www.youtube.com/@dreamsofcode) youtube channel zen shell config and then it has become heavily adapted over time. I really like how minimal and clean it feels. If I'm in a git repo it will adapt to display commit status and push / pull status and if I'm over ssh or in a zmx session it also adapts. 103 + 104 + [Zmx](https://zmx.sh/) is another one of the fancy things I have started using. It is terminal sessions like tmux but without the bloat. It is a super small program but it does pretty much exactly what I want. I have my servers set up so that I can ssh into them with `ssh t.*` and it will use that session. It is super slick for long running processes. 105 + 106 + ### Network stack 107 + 108 + !![router and switch](https://l4.dunkirk.sh/i/JC13Aar6VPg2.webp)[the access point](https://l4.dunkirk.sh/i/PYMlcqfBHHnD.webp)[rpi](https://l4.dunkirk.sh/i/gygT3oV7R8LH.webp){caption="it is fairly basic but it does work"} 109 + 110 + Everything is based on tplink stuff which I rather hate. Their access points are fine but the router and switch are despicable to work with. Their app is even worse if that is even possible. I really love my old setup of using an old workstation to run pfSense with two NICs. It was clean and reliable and the ui was okay to work with. I had to switch it over for my parents so that ostensibly it would be simpler and easier for them to use which as time has proven was very much not true. If money was no object I would love to get a founders edition [Gateway](https://mono.si/) router. You can really tell that it has had heart and soul poured into it and it looks soooooo good as a result. 111 + 112 + As far as routing and port forwarding go I generally try to avoid it as much as humanly possible because of the atrocity of the router ui and as a result I have no ports open at the moment. I used cloudflare tunnels for practically everything at this point though now that I have my own VPS I might start using caddy to tunnel stuff back over tailscale for me instead. 113 + 114 + ### Nest 115 + 116 + ![my static site on nest](https://l4.dunkirk.sh/i/bO5ra0U14_Vh.webp){caption="rendered from my github readme and served by caddy on nest"} 117 + 118 + This is where I used to host most of my throwaway services and slackbots. I still do host a fair amount here but it has got increasingly slower as the user count just keeps going up. It definetly isn't the fault of the admins though. I'm friends with almost all of them and they have done a wonderful job trying to keep it online while it tries to explode itself constantly. It has quite litterally been upgraded at least 4 times now with more and more compute and storage and we keep running out. 119 + 120 + Fully nixos server so there is that :)
+49
content/blog/2026-01-11_frc-rebuilt-calculator.md
···
··· 1 + +++ 2 + title = "FRC REBUILT Points Calculator" 3 + date = 2026-01-11 4 + slug = "frc-rebuilt-calculator" 5 + description = "Interactive calculator for the 2026 FRC game REBUILT" 6 + 7 + [taxonomies] 8 + tags = ["frc", "robotics", "calculator"] 9 + 10 + [extra] 11 + has_toc = false 12 + +++ 13 + 14 + I was manually doing bps calculations yesterday at kickoff and figured there must be a better way so here you go :) 15 + 16 + <!-- more --> 17 + 18 + ### Match Timeline 19 + 20 + A match lasts **2 minutes and 40 seconds** (160 seconds total): 21 + 22 + | Period | Duration | Hub Status | 23 + |--------|----------|------------| 24 + | **Autonomous** | 20s | Both Hubs Active | 25 + | **Transition Shift** | 10s | Both Hubs Active | 26 + | **Shift 1** | 25s | One Active / One Inactive | 27 + | **Shift 2** | 25s | One Active / One Inactive | 28 + | **Shift 3** | 25s | One Active / One Inactive | 29 + | **Shift 4** | 25s | One Active / One Inactive | 30 + | **End Game** | 30s | Both Hubs Active | 31 + 32 + Winning autonomous affects your Hub status so if your alliance scores the most Fuel in AUTO, your Hub is **Inactive** for Shifts 1 & 3, and **Active** for Shifts 2 & 4. 33 + 34 + ### Ranking points 35 + 36 + For Regional/District events: 37 + - **Energized RP:** Score **100 Fuel** (1 RP) 38 + - **Supercharged RP:** Score **360 Fuel** (1 RP) 39 + - **Traversal RP:** Earn **50 Tower points** (1 RP) 40 + 41 + ### BPS calculator 42 + 43 + Use this calculator to determine the balls per second (BPS) your robot needs to achieve ranking point thresholds. Adjust parameters based on your robot's capabilities and strategy. 44 + 45 + The max bps is only used in the simulation of the match while the results section is doing back propagation to figure out the necessary BPS needed to hit that ranking point no matter how high that is. If one of the results says "N/A" that means that your reload time is too high and eats up enough shooting time its no longer possible to hit that ranking point threshold. 46 + 47 + {{ frcRebuilt() }} 48 + 49 + Hopefully this can help your team! May your BPS be ever optimal.
-102
content/blog/monaspace-vs-code-install.md
··· 1 - +++ 2 - title = "Monaspace VS-Code install" 3 - date = 2023-11-10 4 - slug = "monaspace-vs-code-install" 5 - description = "How to install the Github Next team's Monaspace font in VSCode" 6 - 7 - [taxonomies] 8 - tags = ["tutorial", "archival"] 9 - 10 - [extra] 11 - has_toc = true 12 - +++ 13 - 14 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/KuOAwCEm9ypWEemv60Qs7.png" alt="monaspace font in action" caption="This font is so pretty and has so many features its amazing. It's main downside is to work it takes to set it up.") }} 15 - 16 - To install the Monaspace font on macOS (or windows or linux) with VS Code and enable multifont syntax highlighting with the [CSS JS Loader extension](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css), you can follow these steps: 17 - 18 - ## 1. Download and install the Monaspace font: 19 - 20 - First visit [https://github.com/githubnext/monaspace/releases/latest](https://github.com/githubnext/monaspace/releases/latest) and download the zip. 21 - Next to install the Monaspace font: 22 - - On macOS, drag the font files into font book. 23 - - For windows, drag into the font window in settings. 24 - - For Linux, clone the repo and run: `cd util; ./install_linux.sh` 25 - 26 - ## 2. Configure VS Code 27 - 28 - Install the [Custom CSS and JS Loader](https://marketplace.visualstudio.com/items?itemName=be5invis.vscode-custom-css) plugin. 29 - Set the font to one of the following options: `Monaspace Neon Var`, `Monaspace Argon Var`, `Monaspace Xeon Var`, `Monaspace Radon Var`, or `Monaspace Krypton Var`. 30 - 31 - - You will find this option under _Editor: Font Family_ in the user preferences 32 - 33 - {{ img(id="https://assets.vrite.io/64974cb888e8beebeb2c925b/v0cMm5jcwHEgrvtBv4Syx.png" alt="the available varients of the font") }} 34 - 35 - 36 - Next enable font ligatures in the settings.json with following snippet: 37 - 38 - > settings.json 39 - ```json 40 - "editor.fontLigatures": "'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', calt', 'dlig'", 41 - ``` 42 - Now enable the custom CSS file within the `settings.json`, modifying the file path for Windows / MacOS / Linux if needed: 43 - 44 - > still settings.json 45 - ```json 46 - "vscode_custom_css.imports": [ 47 - "file:///Users/{{user}}/.vscode/style.css", // for mac (remove if not mac) 48 - "file://C://Users/{{user}}/vscode/style.css" // for windows (remove if not windows) 49 - "file:///home/{{user}}/.vscode/style.css" // for linux (remove if not windows) 50 - ], 51 - ``` 52 - 53 - ## 3. Create custom CSS file at the path you specified above. 54 - 55 - Depending on your VS Code version, the class names might be different, so you may need to use the developer tools to find the correct one. 56 - The styles that worked for me on `VS Code version: 1.84.2 (Universal) commit: 1a5daa3a0231a0fbba4f14db7ec463cf99d7768e` are here: 57 - 58 - > style.css 59 - ```css 60 - /* Comment Class */ 61 - .mtk3 { 62 - font-family: "Monaspace Radon Var"; 63 - font-weight: 500; 64 - } 65 - 66 - /* Copilot Classes */ 67 - .ghost-text-decoration { 68 - font-family: "Monaspace Krypton Var"; 69 - font-weight: 200; 70 - } 71 - 72 - .ghost-text-decoration-preview { 73 - font-family: "Monaspace Krypton Var"; 74 - font-weight: 200; 75 - } 76 - ``` 77 - 78 - *Thanks to **[@fspoettel](https://github.com/fspoettel)** on GitHub for this trick to get the copilot classes when in dev mode* 79 - 80 - > "You can inspect transient DOM elements by halting the app with a `debugger` after a delay with a debugger call inside a `setTimeout`." 81 - > 82 - > <cite>[@fspoettel](https://github.com/fspoettel)</cite> 83 - 84 - You can copy the following snippet to do just that! 85 - 86 - > console 87 - ```ts 88 - setTimeout(() => { 89 - debugger; 90 - }, 10000); 91 - ``` 92 - 93 - Before you are finished make sure you have run the `Enable Custom CSS and JS` command from the command bar. 94 - 95 - ## Closing Remarks 96 - 97 - That should be it! Hopefully you will have a beautiful custom font VS Code install. 98 - 99 - If you are looking for a good theme, I can highly recommend the [Catppuccin](https://marketplace.visualstudio.com/items?itemName=Catppuccin.catppuccin-vsc) theme, as that is what I use myself. Be sure to check out [Monaspaceโ€™s webstite](https://monaspace.githubnext.com/) as it is a work of art. Happy Coding! ๐Ÿ‘ฉโ€๐Ÿ’ป 100 - 101 - * *Updated 2024-08-22: changed mtk4 to mtk3 on the feedback of [mutammim](https://github.com/mutammim)* 102 - * *Updated 2024-10-31: changed around the formating of the post and moved to [dunkirk.sh](https://dunkirk.sh)*
···
+30 -2
content/pfp.md
··· 4 5 All my profile pictures over the years 6 7 # 2024 8 9 - October to present 10 11 - ![kieran with a white and gray spotted kitten](/pfps/current.webp) 12 13 late September to early October 14
··· 4 5 All my profile pictures over the years 6 7 + # 2025 8 + 9 + November 11th till present 10 + 11 + ![kieran with a robotics sweatshirt and fall leaves behind](/pfps/fall.webp) 12 + 13 + September 13th till November 11th 14 + 15 + ![kieran with his hands framing his face](/pfps/hands.jpg) 16 + 17 + June to September 13th 18 + 19 + dynamically updating varient of the starry background one with the cat; the background would change with the time. 20 + 21 + {{ bluesky(post="https://bsky.app/profile/serif.blue/post/3lqncouklcc2e") }} 22 + 23 + February 27th to June 24 + 25 + ![kieran with an orange cast in a polaroid over a pinkish background](/pfps/instsqc-rat-pfp.webp) 26 + 27 + February 24th to 27th 28 + 29 + ![kieran as a rat hand drawn in black over a purple gradient background](/pfps/kieranrat.webp) 30 + 31 # 2024 32 33 + End of November to 2025 February 34 35 + ![kieran with a white and gray spotted kitten with a grainy background and star dust](/pfps/starry.webp) 36 + 37 + October to very on of November 38 + 39 + ![kieran with a white and gray spotted kitten](/pfps/kitty.webp) 40 41 late September to early October 42
+30
content/verify.md
···
··· 1 + +++ 2 + title = "slash verify" 3 + +++ 4 + 5 + Inspired by [@Molly White](https://www.mollywhite.net/verify/) and [@Rob Knight](https://rknight.me/verify) this page serves as verification of my various accounts. 6 + 7 + # domains / email 8 + 9 + I personally own and control this domain ([dunkirk.sh](https://dunkirk.sh)) as well as [kieranklukas.com](https://kieranklukas.com) and maintain email addresses on both domains. I also just got [serif.blue](https://serif.blue) which I'm super excited about! I have a redirect from [kieran.klukas.net](https://kieran.klukas.net) to dunkirk.sh that is also maintained. 10 + 11 + If you want to contact me, via email to `kieran@dunkirk.sh`. I also send email from `kieran@hackclub.com` and ~`kieran@purplebubble.org`~. 12 + 13 + # accounts 14 + 15 + - Keyoxide: [aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A](https://keyoxide.org/aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A) 16 + - Github: [@taciturnaxolotl](https://github.com/taciturnaxolotl) (formerly @kcoderhtml) 17 + - [Hackclub Slack](https://hackclub.com/slack/): [@krn](https://hackclub.slack.com/team/U062UG485EE) (display name changes quite often though) with userID `U062UG485EE` 18 + - Bluesky: [@dunkirk.sh](https://bsky.app/profile/dunkirk.sh) 19 + - Mastodon: [@taciturnaxolotl@social.dino.icu](https://social.dino.icu/@taciturnaxolotl) 20 + - Youtube: [@kieran.rambles](https://www.youtube.com/@kieran.rambles) 21 + - Matrix: ~[@kieran:dumpsterfire.icu](https://matrix.to/#/@kieran.matrix.dumpsterfire.icu)~ or [@sclacker:matrix.org](https://matrix.to/#/@sclacker:matrix.org) (i'm active on here once in a blue moon so this isn't a great way to contact me urgently) 22 + - Signal: `verox.89` 23 + 24 + # keys 25 + 26 + > SSH 27 + 28 + ```pub 29 + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzEEjvbL/ttqmYoDjxYQmDIq36BabROJoXgQKeh9liBxApwp+2PmgxROzTg42UrRc9pyrkq5kVfxG5hvkqCinhL1fMiowCSEs2L2/Cwi40g5ZU+QwdcwI8a4969kkI46PyB19RHkxg54OUORiIiso/WHGmqQsP+5wbV0+4riSnxwn/JXN4pmnE//stnyAyoiEZkPvBtwJjKb3Ni9n3eNLNs6gnaXrCtaygEZdebikr9kS2g9mM696HvIFgM6cdR/wZ7DcLbG3IdTXuHN7PC3xxL+Y4ek5iMreQIPmuvs4qslbthPGYoYbYLUQiRa9XO5s/ksIj5Z14f7anHE6cuTQVpvNWdGDOigyIVS5qU+4ZF7j+rifzOXVL48gmcAvw/uV68m5Wl/p0qsC/d8vI3GYwEsWG/EzpAlc07l8BU2LxWgN+d7uwBFaJV9VtmUDs5dcslsh8IbzmtC9gq3OLGjklxTfIl6qPiL8U33oc/UwqzvZUrI2BlbagvIZYy6rP+q0= me@dunkirk.sh 30 + ```
+1744
highlight_themes/Wildlife.tmTheme
···
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <!-- Generated by: TmTheme-Editor --> 4 + <!-- ============================================ --> 5 + <!-- app: http://tmtheme-editor.herokuapp.com --> 6 + <!-- code: https://github.com/aziz/tmTheme-Editor --> 7 + <plist version="1.0"> 8 + <dict> 9 + <key>author</key> 10 + <string>Taiwo Kareem</string> 11 + <key>name</key> 12 + <string>Wildlife</string> 13 + <key>settings</key> 14 + <array> 15 + <dict> 16 + <key>settings</key> 17 + <dict> 18 + <key>activeGuide</key> 19 + <string></string> 20 + <key>background</key> 21 + <string>#FFFFFF</string> 22 + <key>caret</key> 23 + <string>#FF00FF</string> 24 + <key>foreground</key> 25 + <string>#000000</string> 26 + <key>findHighlight</key> 27 + <string>#00BFFF</string> 28 + <key>findHighlightForeground</key> 29 + <string>#f0f0f0</string> 30 + <key>gutter</key> 31 + <string></string> 32 + <key>inactiveSelection</key> 33 + <string>#ffcc9c</string> 34 + <key>invisibles</key> 35 + <string>#BFBFBF</string> 36 + <key>lineHighlight</key> 37 + <string>#00000012</string> 38 + <key>selectionBorder</key> 39 + <string>#E996F7</string> 40 + <key>selection</key> 41 + <string>#BAD6FD</string> 42 + <key>stackGuide</key> 43 + <string>#abcdef</string> 44 + <key>shadow</key> 45 + <string>#ff000011</string> 46 + <key>bracketContentsForeground</key> 47 + <string>#6600FF</string> 48 + <key>bracketContentsOptions</key> 49 + <string>stippled_underline</string> 50 + <key>bracketsForeground</key> 51 + <string>#800080</string> 52 + <key>tagsForeground</key> 53 + <string>#6600FF</string> 54 + </dict> 55 + </dict> 56 + <dict> 57 + <key>name</key> 58 + <string>Comment</string> 59 + <key>scope</key> 60 + <string>comment</string> 61 + <key>settings</key> 62 + <dict> 63 + <key>background</key> 64 + <string>#F5FFFF</string> 65 + <key>foreground</key> 66 + <string>#919191</string> 67 + <key>fontStyle</key> 68 + <string>italic</string> 69 + </dict> 70 + </dict> 71 + <dict> 72 + <key>name</key> 73 + <string>String</string> 74 + <key>scope</key> 75 + <string>string</string> 76 + <key>settings</key> 77 + <dict> 78 + <key>foreground</key> 79 + <string>#00A33F</string> 80 + </dict> 81 + </dict> 82 + <dict> 83 + <key>name</key> 84 + <string>Number</string> 85 + <key>scope</key> 86 + <string>constant.numeric</string> 87 + <key>settings</key> 88 + <dict> 89 + </dict> 90 + </dict> 91 + <dict> 92 + <key>name</key> 93 + <string>Built-In Constant</string> 94 + <key>scope</key> 95 + <string>constant.language</string> 96 + <key>settings</key> 97 + <dict> 98 + <key>foreground</key> 99 + <string>#C0D</string> 100 + </dict> 101 + </dict> 102 + <dict> 103 + <key>name</key> 104 + <string>User-Defined Constant</string> 105 + <key>scope</key> 106 + <string>constant.character, constant.other</string> 107 + <key>settings</key> 108 + <dict> 109 + </dict> 110 + </dict> 111 + <dict> 112 + <key>name</key> 113 + <string>Variable</string> 114 + <key>scope</key> 115 + <string>variable.language, variable.other</string> 116 + <key>settings</key> 117 + <dict> 118 + </dict> 119 + </dict> 120 + <dict> 121 + <key>name</key> 122 + <string>Keyword</string> 123 + <key>scope</key> 124 + <string>keyword</string> 125 + <key>settings</key> 126 + <dict> 127 + <key>foreground</key> 128 + <string>#FF5600</string> 129 + </dict> 130 + </dict> 131 + <dict> 132 + <key>name</key> 133 + <string>Storage</string> 134 + <key>scope</key> 135 + <string>storage</string> 136 + <key>settings</key> 137 + <dict> 138 + <key>foreground</key> 139 + <string>#FF5600</string> 140 + </dict> 141 + </dict> 142 + <dict> 143 + <key>name</key> 144 + <string>Type Name</string> 145 + <key>scope</key> 146 + <string>entity.name.type</string> 147 + <key>settings</key> 148 + <dict> 149 + <key>foreground</key> 150 + <string>#21439C</string> 151 + </dict> 152 + </dict> 153 + <dict> 154 + <key>name</key> 155 + <string>Inherited Class</string> 156 + <key>scope</key> 157 + <string>entity.other.inherited-class</string> 158 + <key>settings</key> 159 + <dict> 160 + <key>foreground</key> 161 + <string>#21439C</string> 162 + <key>fontStyle</key> 163 + <string>bold</string> 164 + </dict> 165 + </dict> 166 + <dict> 167 + <key>name</key> 168 + <string>Function Name</string> 169 + <key>scope</key> 170 + <string>entity.name.function</string> 171 + <key>settings</key> 172 + <dict> 173 + <key>foreground</key> 174 + <string>#21439C</string> 175 + <key>fontStyle</key> 176 + <string>italic</string> 177 + </dict> 178 + </dict> 179 + <dict> 180 + <key>name</key> 181 + <string>Function Argument</string> 182 + <key>scope</key> 183 + <string>variable.parameter</string> 184 + <key>settings</key> 185 + <dict> 186 + <key>foreground</key> 187 + <string>#993CF3</string> 188 + </dict> 189 + </dict> 190 + <dict> 191 + <key>name</key> 192 + <string>Tag Name</string> 193 + <key>scope</key> 194 + <string>entity.name.tag, punctuation.definition.tag</string> 195 + <key>settings</key> 196 + <dict> 197 + <key>foreground</key> 198 + <string>#0000FF</string> 199 + </dict> 200 + </dict> 201 + <dict> 202 + <key>name</key> 203 + <string>Tag Attribute</string> 204 + <key>scope</key> 205 + <string>entity.other.attribute-name</string> 206 + <key>settings</key> 207 + <dict> 208 + <key>foreground</key> 209 + <string>#800080</string> 210 + </dict> 211 + </dict> 212 + <dict> 213 + <key>name</key> 214 + <string>Library Function</string> 215 + <key>scope</key> 216 + <string>support.function</string> 217 + <key>settings</key> 218 + <dict> 219 + <key>foreground</key> 220 + <string>#C0D</string> 221 + </dict> 222 + </dict> 223 + <dict> 224 + <key>name</key> 225 + <string>Library Constant</string> 226 + <key>scope</key> 227 + <string>support.constant</string> 228 + <key>settings</key> 229 + <dict> 230 + <key>foreground</key> 231 + <string>#C0D</string> 232 + </dict> 233 + </dict> 234 + <dict> 235 + <key>name</key> 236 + <string>Library Class&#x2f;Type</string> 237 + <key>scope</key> 238 + <string>support.type, support.class</string> 239 + <key>settings</key> 240 + <dict> 241 + <key>foreground</key> 242 + <string>#C0D</string> 243 + </dict> 244 + </dict> 245 + <dict> 246 + <key>name</key> 247 + <string>Library Variable</string> 248 + <key>scope</key> 249 + <string>support.variable</string> 250 + <key>settings</key> 251 + <dict> 252 + <key>foreground</key> 253 + <string>#C0D</string> 254 + </dict> 255 + </dict> 256 + <dict> 257 + <key>name</key> 258 + <string>Invalid</string> 259 + <key>scope</key> 260 + <string>invalid</string> 261 + <key>settings</key> 262 + <dict> 263 + <key>background</key> 264 + <string>#990000</string> 265 + <key>foreground</key> 266 + <string>#FFFFFF</string> 267 + </dict> 268 + </dict> 269 + <dict> 270 + <key>name</key> 271 + <string>String Interpolation</string> 272 + <key>scope</key> 273 + <string>constant.other.placeholder</string> 274 + <key>settings</key> 275 + <dict> 276 + <key>foreground</key> 277 + <string>#990000</string> 278 + <key>fontStyle</key> 279 + <string>italic</string> 280 + </dict> 281 + </dict> 282 + <dict> 283 + <key>name</key> 284 + <string>Text: Embedded Embedded Source</string> 285 + <key>scope</key> 286 + <string>text source text source</string> 287 + <key>settings</key> 288 + <dict> 289 + <key>background</key> 290 + <string>#EFEFFF</string> 291 + </dict> 292 + </dict> 293 + <dict> 294 + <key>name</key> 295 + <string>Markup: Header</string> 296 + <key>scope</key> 297 + <string>meta.header, markup.heading</string> 298 + <key>settings</key> 299 + <dict> 300 + <key>foreground</key> 301 + <string>#800080</string> 302 + <key>fontStyle</key> 303 + <string>bold</string> 304 + </dict> 305 + </dict> 306 + <dict> 307 + <key>name</key> 308 + <string>Git Gutter: Deleted</string> 309 + <key>scope</key> 310 + <string>markup.deleted.git_gutter</string> 311 + <key>settings</key> 312 + <dict> 313 + <key>foreground</key> 314 + <string>#F92672</string> 315 + </dict> 316 + </dict> 317 + <dict> 318 + <key>name</key> 319 + <string>Git Gutter: Inserted</string> 320 + <key>scope</key> 321 + <string>markup.inserted.git_gutter</string> 322 + <key>settings</key> 323 + <dict> 324 + <key>foreground</key> 325 + <string>#22AA22</string> 326 + </dict> 327 + </dict> 328 + <dict> 329 + <key>name</key> 330 + <string>Git Gutter: Changed</string> 331 + <key>scope</key> 332 + <string>markup.changed.git_gutter</string> 333 + <key>settings</key> 334 + <dict> 335 + <key>foreground</key> 336 + <string>#660066</string> 337 + </dict> 338 + </dict> 339 + <dict> 340 + <key>name</key> 341 + <string>Git Gutter: Ignored</string> 342 + <key>scope</key> 343 + <string>markup.ignored.git_gutter</string> 344 + <key>settings</key> 345 + <dict> 346 + <key>foreground</key> 347 + <string>#3333CC</string> 348 + </dict> 349 + </dict> 350 + <dict> 351 + <key>name</key> 352 + <string>Git Gutter: Untracked</string> 353 + <key>scope</key> 354 + <string>markup.untracked.git_gutter</string> 355 + <key>settings</key> 356 + <dict> 357 + <key>foreground</key> 358 + <string>#00CC99</string> 359 + </dict> 360 + </dict> 361 + <dict> 362 + <key>name</key> 363 + <string>Sublimelinter: Annotations</string> 364 + <key>scope</key> 365 + <string>sublimelinter.annotations</string> 366 + <key>settings</key> 367 + <dict> 368 + <key>background</key> 369 + <string>#FFFFAA</string> 370 + <key>foreground</key> 371 + <string>#FFFFFF</string> 372 + </dict> 373 + </dict> 374 + <dict> 375 + <key>name</key> 376 + <string>Sublimelinter: Error Outline</string> 377 + <key>scope</key> 378 + <string>sublimelinter.outline.illegal</string> 379 + <key>settings</key> 380 + <dict> 381 + <key>background</key> 382 + <string>#FF4A52</string> 383 + <key>foreground</key> 384 + <string>#FFFFFF</string> 385 + </dict> 386 + </dict> 387 + <dict> 388 + <key>name</key> 389 + <string>Sublimelinter: Error Underline</string> 390 + <key>scope</key> 391 + <string>sublimelinter.underline.illegal</string> 392 + <key>settings</key> 393 + <dict> 394 + <key>background</key> 395 + <string>#FF0000</string> 396 + </dict> 397 + </dict> 398 + <dict> 399 + <key>name</key> 400 + <string>Sublimelinter: Warning Outline</string> 401 + <key>scope</key> 402 + <string>sublimelinter.outline.warning</string> 403 + <key>settings</key> 404 + <dict> 405 + <key>background</key> 406 + <string>#DF9400</string> 407 + <key>foreground</key> 408 + <string>#FFFFFF</string> 409 + </dict> 410 + </dict> 411 + <dict> 412 + <key>name</key> 413 + <string>Sublimelinter: Warning Underline</string> 414 + <key>scope</key> 415 + <string>sublimelinter.underline.warning</string> 416 + <key>settings</key> 417 + <dict> 418 + <key>background</key> 419 + <string>#FF0000</string> 420 + </dict> 421 + </dict> 422 + <dict> 423 + <key>name</key> 424 + <string>Sublimelinter: Violation Outline</string> 425 + <key>scope</key> 426 + <string>sublimelinter.outline.violation</string> 427 + <key>settings</key> 428 + <dict> 429 + <key>background</key> 430 + <string>#FFFFFF33</string> 431 + <key>foreground</key> 432 + <string>#FFFFFF</string> 433 + </dict> 434 + </dict> 435 + <dict> 436 + <key>name</key> 437 + <string>Sublimelinter: Violation Underline</string> 438 + <key>scope</key> 439 + <string>sublimelinter.underline.violation</string> 440 + <key>settings</key> 441 + <dict> 442 + <key>background</key> 443 + <string>#FF0000</string> 444 + </dict> 445 + </dict> 446 + <dict> 447 + <key>name</key> 448 + <string>Sublimelinter: Gutter Mark</string> 449 + <key>scope</key> 450 + <string>sublimelinter.gutter-mark</string> 451 + <key>settings</key> 452 + <dict> 453 + <key>foreground</key> 454 + <string>#FF0000</string> 455 + </dict> 456 + </dict> 457 + <dict> 458 + <key>name</key> 459 + <string>Sublimelinter: Error</string> 460 + <key>scope</key> 461 + <string>sublimelinter.mark.error</string> 462 + <key>settings</key> 463 + <dict> 464 + <key>foreground</key> 465 + <string>#D02000</string> 466 + </dict> 467 + </dict> 468 + <dict> 469 + <key>name</key> 470 + <string>Sublimelinter: Warning</string> 471 + <key>scope</key> 472 + <string>sublimelinter.mark.warning</string> 473 + <key>settings</key> 474 + <dict> 475 + <key>foreground</key> 476 + <string>#DDB700</string> 477 + </dict> 478 + </dict> 479 + <dict> 480 + <key>name</key> 481 + <string>Character Escape</string> 482 + <key>scope</key> 483 + <string>constant.character.escape</string> 484 + <key>settings</key> 485 + <dict> 486 + <key>background</key> 487 + <string>#EFEAF4</string> 488 + <key>foreground</key> 489 + <string>#4C4D00</string> 490 + <key>fontStyle</key> 491 + <string>italic bold</string> 492 + </dict> 493 + </dict> 494 + <dict> 495 + <key>name</key> 496 + <string>Language Constants</string> 497 + <key>scope</key> 498 + <string>constant.language, constant.character, constant.other</string> 499 + <key>settings</key> 500 + <dict> 501 + <key>fontStyle</key> 502 + <string>italic</string> 503 + </dict> 504 + </dict> 505 + <dict> 506 + <key>name</key> 507 + <string>Numbers</string> 508 + <key>scope</key> 509 + <string>constant.numeric</string> 510 + <key>settings</key> 511 + <dict> 512 + <key>foreground</key> 513 + <string>#0000FF</string> 514 + <key>fontStyle</key> 515 + <string>italic</string> 516 + </dict> 517 + </dict> 518 + <dict> 519 + <key>name</key> 520 + <string>Preprocessor</string> 521 + <key>scope</key> 522 + <string>meta.preprocessor, meta.preprocessor</string> 523 + <key>settings</key> 524 + <dict> 525 + <key>foreground</key> 526 + <string>#008000</string> 527 + <key>fontStyle</key> 528 + <string>italic</string> 529 + </dict> 530 + </dict> 531 + <dict> 532 + <key>name</key> 533 + <string>Punctuations: Comments</string> 534 + <key>scope</key> 535 + <string>punctuation.definition.comment</string> 536 + <key>settings</key> 537 + <dict> 538 + <key>fontStyle</key> 539 + <string>bold italic</string> 540 + </dict> 541 + </dict> 542 + <dict> 543 + <key>name</key> 544 + <string>Punctuations: Strings</string> 545 + <key>scope</key> 546 + <string>punctuation.definition.string</string> 547 + <key>settings</key> 548 + <dict> 549 + </dict> 550 + </dict> 551 + <dict> 552 + <key>name</key> 553 + <string>Punctuations: Parameters&#x2f;Preprocessors</string> 554 + <key>scope</key> 555 + <string>punctuation.definition.parameters, punctuation.definition.preprocessor, meta.preprocessor</string> 556 + <key>settings</key> 557 + <dict> 558 + <key>fontStyle</key> 559 + <string></string> 560 + </dict> 561 + </dict> 562 + <dict> 563 + <key>name</key> 564 + <string>Storage&#x2f;Antlr: Extends</string> 565 + <key>scope</key> 566 + <string>storage.modifier, meta.definition.class.extends.antlr </string> 567 + <key>settings</key> 568 + <dict> 569 + <key>fontStyle</key> 570 + <string>italic</string> 571 + </dict> 572 + </dict> 573 + <dict> 574 + <key>name</key> 575 + <string>Support: Class</string> 576 + <key>scope</key> 577 + <string>support.class</string> 578 + <key>settings</key> 579 + <dict> 580 + <key>foreground</key> 581 + <string>#F12958</string> 582 + <key>fontStyle</key> 583 + <string>italic</string> 584 + </dict> 585 + </dict> 586 + <dict> 587 + <key>name</key> 588 + <string>Punctuations: Arguments</string> 589 + <key>scope</key> 590 + <string>punctuation.definition.arguments</string> 591 + <key>settings</key> 592 + <dict> 593 + <key>foreground</key> 594 + <string>#26004C</string> 595 + <key>fontStyle</key> 596 + <string>italic</string> 597 + </dict> 598 + </dict> 599 + <dict> 600 + <key>name</key> 601 + <string>Antlr: Class&#x2f;Options</string> 602 + <key>scope</key> 603 + <string>meta.definition.class.antlr, meta.options-block.antlr, meta.options.antlr, meta.rule.antlr</string> 604 + <key>settings</key> 605 + <dict> 606 + <key>foreground</key> 607 + <string>#A535AE</string> 608 + </dict> 609 + </dict> 610 + <dict> 611 + <key>name</key> 612 + <string>Antlr: Rule</string> 613 + <key>scope</key> 614 + <string>meta.rule.antlr</string> 615 + <key>settings</key> 616 + <dict> 617 + <key>foreground</key> 618 + <string>#080</string> 619 + </dict> 620 + </dict> 621 + <dict> 622 + <key>name</key> 623 + <string>Antlr: Punctuations-Section</string> 624 + <key>scope</key> 625 + <string>punctuation.section.group.antlr, punctuation.section.options.antlr</string> 626 + <key>settings</key> 627 + <dict> 628 + <key>foreground</key> 629 + <string>#00004C</string> 630 + </dict> 631 + </dict> 632 + <dict> 633 + <key>name</key> 634 + <string>C++: Destructor</string> 635 + <key>scope</key> 636 + <string>meta.function.destructor</string> 637 + <key>settings</key> 638 + <dict> 639 + <key>foreground</key> 640 + <string>#800080</string> 641 + <key>fontStyle</key> 642 + <string>bold</string> 643 + </dict> 644 + </dict> 645 + <dict> 646 + <key>name</key> 647 + <string>C++: Arguments</string> 648 + <key>scope</key> 649 + <string>meta.parens</string> 650 + <key>settings</key> 651 + <dict> 652 + <key>foreground</key> 653 + <string>#993CF3</string> 654 + <key>fontStyle</key> 655 + <string>italic</string> 656 + </dict> 657 + </dict> 658 + <dict> 659 + <key>name</key> 660 + <string>C++: Constructor Initializer Lists</string> 661 + <key>scope</key> 662 + <string>meta.function.constructor.initializer-list</string> 663 + <key>settings</key> 664 + <dict> 665 + <key>background</key> 666 + <string>#FFFFE0</string> 667 + <key>foreground</key> 668 + <string>#BA064E</string> 669 + <key>fontStyle</key> 670 + <string>italic</string> 671 + </dict> 672 + </dict> 673 + <dict> 674 + <key>name</key> 675 + <string>C: Prototypes</string> 676 + <key>scope</key> 677 + <string>meta.function.prototype</string> 678 + <key>settings</key> 679 + <dict> 680 + <key>fontStyle</key> 681 + <string>bold</string> 682 + </dict> 683 + </dict> 684 + <dict> 685 + <key>name</key> 686 + <string>C: Constructor</string> 687 + <key>scope</key> 688 + <string>meta.function.constructor</string> 689 + <key>settings</key> 690 + <dict> 691 + <key>fontStyle</key> 692 + <string>bold</string> 693 + </dict> 694 + </dict> 695 + <dict> 696 + <key>name</key> 697 + <string>Cold Fusion: Sql Embedded</string> 698 + <key>scope</key> 699 + <string>source.sql.embedded</string> 700 + <key>settings</key> 701 + <dict> 702 + <key>background</key> 703 + <string>#FFF6F6</string> 704 + </dict> 705 + </dict> 706 + <dict> 707 + <key>name</key> 708 + <string>Css: Property Lists</string> 709 + <key>scope</key> 710 + <string>meta.property-list.css</string> 711 + <key>settings</key> 712 + <dict> 713 + <key>background</key> 714 + <string>#FFF4FF</string> 715 + </dict> 716 + </dict> 717 + <dict> 718 + <key>name</key> 719 + <string>Css: Property Name</string> 720 + <key>scope</key> 721 + <string>meta.property-name.css, support.type.property-name.css</string> 722 + <key>settings</key> 723 + <dict> 724 + <key>foreground</key> 725 + <string>#FF5555</string> 726 + </dict> 727 + </dict> 728 + <dict> 729 + <key>name</key> 730 + <string>Css: Property Value</string> 731 + <key>scope</key> 732 + <string>meta.property-value.css, support.constant.property-value.css</string> 733 + <key>settings</key> 734 + <dict> 735 + <key>fontStyle</key> 736 + <string>italic</string> 737 + </dict> 738 + </dict> 739 + <dict> 740 + <key>name</key> 741 + <string>Css: Colors</string> 742 + <key>scope</key> 743 + <string>constant.other.color, support.constant.color</string> 744 + <key>settings</key> 745 + <dict> 746 + <key>foreground</key> 747 + <string>#8000FF</string> 748 + <key>fontStyle</key> 749 + <string></string> 750 + </dict> 751 + </dict> 752 + <dict> 753 + <key>name</key> 754 + <string>Css: Attribute Names</string> 755 + <key>scope</key> 756 + <string>entity.other.attribute-name</string> 757 + <key>settings</key> 758 + <dict> 759 + <key>foreground</key> 760 + <string>#0080FF</string> 761 + <key>fontStyle</key> 762 + <string>italic</string> 763 + </dict> 764 + </dict> 765 + <dict> 766 + <key>name</key> 767 + <string>Css: Variable Parameters</string> 768 + <key>scope</key> 769 + <string>variable.parameter.misc.css</string> 770 + <key>settings</key> 771 + <dict> 772 + <key>foreground</key> 773 + <string>#00AA00</string> 774 + <key>fontStyle</key> 775 + <string>italic</string> 776 + </dict> 777 + </dict> 778 + <dict> 779 + <key>name</key> 780 + <string>Csv: Row Header</string> 781 + <key>scope</key> 782 + <string>meta.tabular.row.header.csv</string> 783 + <key>settings</key> 784 + <dict> 785 + <key>fontStyle</key> 786 + <string>bold</string> 787 + </dict> 788 + </dict> 789 + <dict> 790 + <key>name</key> 791 + <string>Diff: File Header</string> 792 + <key>scope</key> 793 + <string>meta.diff.header.from-file</string> 794 + <key>settings</key> 795 + <dict> 796 + <key>foreground</key> 797 + <string>#800080</string> 798 + <key>fontStyle</key> 799 + <string>bold</string> 800 + </dict> 801 + </dict> 802 + <dict> 803 + <key>name</key> 804 + <string>Diff: Punctuation Separator</string> 805 + <key>scope</key> 806 + <string>punctuation.definition.separator.diff</string> 807 + <key>settings</key> 808 + <dict> 809 + <key>foreground</key> 810 + <string>#BF6514</string> 811 + <key>fontStyle</key> 812 + <string>bold</string> 813 + </dict> 814 + </dict> 815 + <dict> 816 + <key>name</key> 817 + <string>Diff: Changed Markup</string> 818 + <key>scope</key> 819 + <string>markup.changed.diff</string> 820 + <key>settings</key> 821 + <dict> 822 + <key>background</key> 823 + <string>#FEFEAE</string> 824 + </dict> 825 + </dict> 826 + <dict> 827 + <key>name</key> 828 + <string>Diff: Punctuation Inserted</string> 829 + <key>scope</key> 830 + <string>punctuation.definition.inserted.diff, markup.inserted.diff</string> 831 + <key>settings</key> 832 + <dict> 833 + <key>foreground</key> 834 + <string>#22AA22</string> 835 + </dict> 836 + </dict> 837 + <dict> 838 + <key>name</key> 839 + <string>Diff: Deleted Markup</string> 840 + <key>scope</key> 841 + <string>markup.deleted.diff</string> 842 + <key>settings</key> 843 + <dict> 844 + <key>foreground</key> 845 + <string>#A00000</string> 846 + </dict> 847 + </dict> 848 + <dict> 849 + <key>name</key> 850 + <string>Diff: Toc Line Numbers</string> 851 + <key>scope</key> 852 + <string>meta.toc-list.line-number.diff</string> 853 + <key>settings</key> 854 + <dict> 855 + <key>foreground</key> 856 + <string>#2222FF</string> 857 + <key>fontStyle</key> 858 + <string>italic</string> 859 + </dict> 860 + </dict> 861 + <dict> 862 + <key>name</key> 863 + <string>Diff: Context Range</string> 864 + <key>scope</key> 865 + <string>meta.diff.range.context, meta.diff.range.unified, meta.diff.range.normal</string> 866 + <key>settings</key> 867 + <dict> 868 + <key>foreground</key> 869 + <string>#0000FF</string> 870 + <key>fontStyle</key> 871 + <string>bold</string> 872 + </dict> 873 + </dict> 874 + <dict> 875 + <key>name</key> 876 + <string>Diff: Index</string> 877 + <key>scope</key> 878 + <string>meta.diff.index</string> 879 + <key>settings</key> 880 + <dict> 881 + <key>foreground</key> 882 + <string>#F00</string> 883 + <key>fontStyle</key> 884 + <string>italic</string> 885 + </dict> 886 + </dict> 887 + <dict> 888 + <key>name</key> 889 + <string>Markup: Bold</string> 890 + <key>scope</key> 891 + <string>markup.bold</string> 892 + <key>settings</key> 893 + <dict> 894 + <key>foreground</key> 895 + <string>#0000FF</string> 896 + <key>fontStyle</key> 897 + <string>bold</string> 898 + </dict> 899 + </dict> 900 + <dict> 901 + <key>name</key> 902 + <string>Markup: Italic</string> 903 + <key>scope</key> 904 + <string>markup.italic</string> 905 + <key>settings</key> 906 + <dict> 907 + <key>foreground</key> 908 + <string>#0000FF</string> 909 + <key>fontStyle</key> 910 + <string>italic</string> 911 + </dict> 912 + </dict> 913 + <dict> 914 + <key>name</key> 915 + <string>Markup: List</string> 916 + <key>scope</key> 917 + <string>markup.list, punctuation.definition.list_item</string> 918 + <key>settings</key> 919 + <dict> 920 + <key>background</key> 921 + <string>#FDF5EF</string> 922 + <key>foreground</key> 923 + <string>#800000</string> 924 + </dict> 925 + </dict> 926 + <dict> 927 + <key>name</key> 928 + <string>Markup: Table</string> 929 + <key>scope</key> 930 + <string>markup.other.table, punctuation.definition.table, meta.separator, markup.other, punctuation.definition.field</string> 931 + <key>settings</key> 932 + <dict> 933 + <key>background</key> 934 + <string>#FB89F0</string> 935 + </dict> 936 + </dict> 937 + <dict> 938 + <key>name</key> 939 + <string>Markup: Raw</string> 940 + <key>scope</key> 941 + <string>markup.raw,meta.raw.block, punctuation.definition.raw</string> 942 + <key>settings</key> 943 + <dict> 944 + <key>background</key> 945 + <string>#8FDDF630</string> 946 + </dict> 947 + </dict> 948 + <dict> 949 + <key>name</key> 950 + <string>Markup: Links</string> 951 + <key>scope</key> 952 + <string>markup.underline.link, punctuation.definition.link, meta.link</string> 953 + <key>settings</key> 954 + <dict> 955 + <key>foreground</key> 956 + <string>#C00</string> 957 + </dict> 958 + </dict> 959 + <dict> 960 + <key>name</key> 961 + <string>Markup: Imagelinks</string> 962 + <key>scope</key> 963 + <string>meta.image.inline,meta.image.reference, punctuation.definition.image</string> 964 + <key>settings</key> 965 + <dict> 966 + <key>foreground</key> 967 + <string>#F0F</string> 968 + </dict> 969 + </dict> 970 + <dict> 971 + <key>name</key> 972 + <string>Markup: Quotes</string> 973 + <key>scope</key> 974 + <string>markup.quote</string> 975 + <key>settings</key> 976 + <dict> 977 + <key>background</key> 978 + <string>#FDF5E6</string> 979 + <key>foreground</key> 980 + <string>#228B22</string> 981 + <key>fontStyle</key> 982 + <string>italic</string> 983 + </dict> 984 + </dict> 985 + <dict> 986 + <key>name</key> 987 + <string>Markup: Quotes Punctuation</string> 988 + <key>scope</key> 989 + <string>punctuation.definition.blockquote</string> 990 + <key>settings</key> 991 + <dict> 992 + <key>foreground</key> 993 + <string>#F0F</string> 994 + <key>fontStyle</key> 995 + <string>bold</string> 996 + </dict> 997 + </dict> 998 + <dict> 999 + <key>name</key> 1000 + <string>Dylan: Class&#x2f;Options</string> 1001 + <key>scope</key> 1002 + <string>meta.definition.class, meta.rescue, meta.features</string> 1003 + <key>settings</key> 1004 + <dict> 1005 + <key>foreground</key> 1006 + <string>#FF5600</string> 1007 + </dict> 1008 + </dict> 1009 + <dict> 1010 + <key>name</key> 1011 + <string>Erlang: Directives&#x2f;Expression</string> 1012 + <key>scope</key> 1013 + <string>meta.directive, meta.expression</string> 1014 + <key>settings</key> 1015 + <dict> 1016 + <key>foreground</key> 1017 + <string>#FF5600</string> 1018 + </dict> 1019 + </dict> 1020 + <dict> 1021 + <key>name</key> 1022 + <string>Groovy: Method Call</string> 1023 + <key>scope</key> 1024 + <string>meta.method-call</string> 1025 + <key>settings</key> 1026 + <dict> 1027 + <key>fontStyle</key> 1028 + <string>italic</string> 1029 + </dict> 1030 + </dict> 1031 + <dict> 1032 + <key>name</key> 1033 + <string>Graphics: Graph Attribute</string> 1034 + <key>scope</key> 1035 + <string>support.constant.attribute.graph.dot</string> 1036 + <key>settings</key> 1037 + <dict> 1038 + <key>foreground</key> 1039 + <string>#00F</string> 1040 + </dict> 1041 + </dict> 1042 + <dict> 1043 + <key>name</key> 1044 + <string>Graphics: Node Attribute</string> 1045 + <key>scope</key> 1046 + <string>support.constant.attribute.node.dot</string> 1047 + <key>settings</key> 1048 + <dict> 1049 + <key>foreground</key> 1050 + <string>#800080</string> 1051 + </dict> 1052 + </dict> 1053 + <dict> 1054 + <key>name</key> 1055 + <string>Graphics: Edge Attribute</string> 1056 + <key>scope</key> 1057 + <string>support.constant.attribute.edge.dot</string> 1058 + <key>settings</key> 1059 + <dict> 1060 + <key>foreground</key> 1061 + <string>#97531F</string> 1062 + </dict> 1063 + </dict> 1064 + <dict> 1065 + <key>name</key> 1066 + <string>Haml: Objects</string> 1067 + <key>scope</key> 1068 + <string>meta.section.object.haml</string> 1069 + <key>settings</key> 1070 + <dict> 1071 + <key>foreground</key> 1072 + <string>#FF5600</string> 1073 + </dict> 1074 + </dict> 1075 + <dict> 1076 + <key>name</key> 1077 + <string>Haml: Ruby Embedded Html</string> 1078 + <key>scope</key> 1079 + <string>source.ruby.embedded.html</string> 1080 + <key>settings</key> 1081 + <dict> 1082 + <key>background</key> 1083 + <string>#FFEFD5</string> 1084 + </dict> 1085 + </dict> 1086 + <dict> 1087 + <key>name</key> 1088 + <string>Asp Html: Punctuation Section</string> 1089 + <key>scope</key> 1090 + <string>punctuation.section.embedded</string> 1091 + <key>settings</key> 1092 + <dict> 1093 + <key>foreground</key> 1094 + <string>#55AAFF</string> 1095 + <key>fontStyle</key> 1096 + <string>bold</string> 1097 + </dict> 1098 + </dict> 1099 + <dict> 1100 + <key>name</key> 1101 + <string>Html: Css Tag</string> 1102 + <key>scope</key> 1103 + <string>source.css punctuation.definition.tag</string> 1104 + <key>settings</key> 1105 + <dict> 1106 + <key>foreground</key> 1107 + <string>#F00</string> 1108 + <key>fontStyle</key> 1109 + <string>bold</string> 1110 + </dict> 1111 + </dict> 1112 + <dict> 1113 + <key>name</key> 1114 + <string>Html: Embedded Ruby tag</string> 1115 + <key>scope</key> 1116 + <string>punctuation.section.embedded.ruby</string> 1117 + <key>settings</key> 1118 + <dict> 1119 + <key>foreground</key> 1120 + <string>#800080</string> 1121 + <key>fontStyle</key> 1122 + <string>bold</string> 1123 + </dict> 1124 + </dict> 1125 + <dict> 1126 + <key>name</key> 1127 + <string>Html: Javascript Tag</string> 1128 + <key>scope</key> 1129 + <string>source.js punctuation.definition.tag</string> 1130 + <key>settings</key> 1131 + <dict> 1132 + <key>foreground</key> 1133 + <string>#0A0</string> 1134 + <key>fontStyle</key> 1135 + <string>bold</string> 1136 + </dict> 1137 + </dict> 1138 + <dict> 1139 + <key>name</key> 1140 + <string>Html: Other Tag</string> 1141 + <key>scope</key> 1142 + <string>meta.tag.other punctuation.definition.tag</string> 1143 + <key>settings</key> 1144 + <dict> 1145 + <key>foreground</key> 1146 + <string>#F0F</string> 1147 + <key>fontStyle</key> 1148 + <string>bold</string> 1149 + </dict> 1150 + </dict> 1151 + <dict> 1152 + <key>name</key> 1153 + <string>Html: Entity Character</string> 1154 + <key>scope</key> 1155 + <string>constant.character.entity.html</string> 1156 + <key>settings</key> 1157 + <dict> 1158 + <key>fontStyle</key> 1159 + <string>bold</string> 1160 + </dict> 1161 + </dict> 1162 + <dict> 1163 + <key>name</key> 1164 + <string>Html: Doctype Tag</string> 1165 + <key>scope</key> 1166 + <string>meta.tag.sgml</string> 1167 + <key>settings</key> 1168 + <dict> 1169 + <key>fontStyle</key> 1170 + <string>bold</string> 1171 + </dict> 1172 + </dict> 1173 + <dict> 1174 + <key>name</key> 1175 + <string>Java: Constructors</string> 1176 + <key>scope</key> 1177 + <string>meta.definition.constructor.java, entity.name.function.constructor.java</string> 1178 + <key>settings</key> 1179 + <dict> 1180 + <key>fontStyle</key> 1181 + <string>bold</string> 1182 + </dict> 1183 + </dict> 1184 + <dict> 1185 + <key>name</key> 1186 + <string>Javadoc: Comments</string> 1187 + <key>scope</key> 1188 + <string>comment.block.documentation.javadoc</string> 1189 + <key>settings</key> 1190 + <dict> 1191 + <key>background</key> 1192 + <string>#FFFFCC</string> 1193 + </dict> 1194 + </dict> 1195 + <dict> 1196 + <key>name</key> 1197 + <string>Javadoc: Punctuations</string> 1198 + <key>scope</key> 1199 + <string>punctuation.definition.comment.javadoc, punctuation.definition.comment.begin.javadoc</string> 1200 + <key>settings</key> 1201 + <dict> 1202 + <key>foreground</key> 1203 + <string>#00F</string> 1204 + <key>fontStyle</key> 1205 + <string>bold</string> 1206 + </dict> 1207 + </dict> 1208 + <dict> 1209 + <key>name</key> 1210 + <string>Javadoc: Directive Punctuations</string> 1211 + <key>scope</key> 1212 + <string>punctuation.definition.directive.begin.javadoc, punctuation.definition.directive.end.javadoc</string> 1213 + <key>settings</key> 1214 + <dict> 1215 + <key>foreground</key> 1216 + <string>#963</string> 1217 + <key>fontStyle</key> 1218 + <string>italic bold</string> 1219 + </dict> 1220 + </dict> 1221 + <dict> 1222 + <key>name</key> 1223 + <string>Json:Group2</string> 1224 + <key>scope</key> 1225 + <string>source.json meta meta meta.structure.dictionary string</string> 1226 + <key>settings</key> 1227 + <dict> 1228 + <key>foreground</key> 1229 + <string>#900</string> 1230 + </dict> 1231 + </dict> 1232 + <dict> 1233 + <key>name</key> 1234 + <string>Json:Group3</string> 1235 + <key>scope</key> 1236 + <string>meta meta meta meta meta.structure.dictionary string</string> 1237 + <key>settings</key> 1238 + <dict> 1239 + <key>foreground</key> 1240 + <string>#606</string> 1241 + </dict> 1242 + </dict> 1243 + <dict> 1244 + <key>name</key> 1245 + <string>Json:Group4</string> 1246 + <key>scope</key> 1247 + <string>meta meta meta meta meta meta meta.structure.dictionary string</string> 1248 + <key>settings</key> 1249 + <dict> 1250 + <key>foreground</key> 1251 + <string>#C00</string> 1252 + </dict> 1253 + </dict> 1254 + <dict> 1255 + <key>name</key> 1256 + <string>Json:Group5</string> 1257 + <key>scope</key> 1258 + <string>meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1259 + <key>settings</key> 1260 + <dict> 1261 + <key>foreground</key> 1262 + <string>#0C0</string> 1263 + </dict> 1264 + </dict> 1265 + <dict> 1266 + <key>name</key> 1267 + <string>Json:Group6</string> 1268 + <key>scope</key> 1269 + <string>meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1270 + <key>settings</key> 1271 + <dict> 1272 + <key>foreground</key> 1273 + <string>#663</string> 1274 + </dict> 1275 + </dict> 1276 + <dict> 1277 + <key>name</key> 1278 + <string>Json:Group7</string> 1279 + <key>scope</key> 1280 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1281 + <key>settings</key> 1282 + <dict> 1283 + <key>foreground</key> 1284 + <string>#F06</string> 1285 + </dict> 1286 + </dict> 1287 + <dict> 1288 + <key>name</key> 1289 + <string>Json:Group8</string> 1290 + <key>scope</key> 1291 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1292 + <key>settings</key> 1293 + <dict> 1294 + <key>foreground</key> 1295 + <string>#030</string> 1296 + </dict> 1297 + </dict> 1298 + <dict> 1299 + <key>name</key> 1300 + <string>Json:Group9</string> 1301 + <key>scope</key> 1302 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1303 + <key>settings</key> 1304 + <dict> 1305 + <key>foreground</key> 1306 + <string>#933</string> 1307 + </dict> 1308 + </dict> 1309 + <dict> 1310 + <key>name</key> 1311 + <string>Json:Group10</string> 1312 + <key>scope</key> 1313 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1314 + <key>settings</key> 1315 + <dict> 1316 + <key>foreground</key> 1317 + <string>#69F</string> 1318 + </dict> 1319 + </dict> 1320 + <dict> 1321 + <key>name</key> 1322 + <string>Json:Group11</string> 1323 + <key>scope</key> 1324 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1325 + <key>settings</key> 1326 + <dict> 1327 + <key>foreground</key> 1328 + <string>#F55</string> 1329 + </dict> 1330 + </dict> 1331 + <dict> 1332 + <key>name</key> 1333 + <string>Json:Group12</string> 1334 + <key>scope</key> 1335 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1336 + <key>settings</key> 1337 + <dict> 1338 + <key>foreground</key> 1339 + <string>#C90</string> 1340 + </dict> 1341 + </dict> 1342 + <dict> 1343 + <key>name</key> 1344 + <string>Json:Group13</string> 1345 + <key>scope</key> 1346 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1347 + <key>settings</key> 1348 + <dict> 1349 + <key>foreground</key> 1350 + <string>#0C9</string> 1351 + </dict> 1352 + </dict> 1353 + <dict> 1354 + <key>name</key> 1355 + <string>Json:Group14</string> 1356 + <key>scope</key> 1357 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1358 + <key>settings</key> 1359 + <dict> 1360 + <key>foreground</key> 1361 + <string>#F00</string> 1362 + </dict> 1363 + </dict> 1364 + <dict> 1365 + <key>name</key> 1366 + <string>Json:Group15</string> 1367 + <key>scope</key> 1368 + <string>meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary string</string> 1369 + <key>settings</key> 1370 + <dict> 1371 + <key>foreground</key> 1372 + <string>#F93</string> 1373 + </dict> 1374 + </dict> 1375 + <dict> 1376 + <key>name</key> 1377 + <string>Mail: Encoding</string> 1378 + <key>scope</key> 1379 + <string>meta.encoded-text.mail</string> 1380 + <key>settings</key> 1381 + <dict> 1382 + <key>foreground</key> 1383 + <string>#9C0</string> 1384 + <key>fontStyle</key> 1385 + <string>italic</string> 1386 + </dict> 1387 + </dict> 1388 + <dict> 1389 + <key>name</key> 1390 + <string>Makefile: Other Variables</string> 1391 + <key>scope</key> 1392 + <string>variable.other.makefile</string> 1393 + <key>settings</key> 1394 + <dict> 1395 + <key>foreground</key> 1396 + <string>#800080</string> 1397 + </dict> 1398 + </dict> 1399 + <dict> 1400 + <key>name</key> 1401 + <string>Makefile: Separator Punctuation</string> 1402 + <key>scope</key> 1403 + <string>punctuation.separator.continuation.makefile</string> 1404 + <key>settings</key> 1405 + <dict> 1406 + <key>foreground</key> 1407 + <string>#F90</string> 1408 + </dict> 1409 + </dict> 1410 + <dict> 1411 + <key>name</key> 1412 + <string>Makefile: Function</string> 1413 + <key>scope</key> 1414 + <string>meta.function.makefile</string> 1415 + <key>settings</key> 1416 + <dict> 1417 + <key>foreground</key> 1418 + <string>#FF5600</string> 1419 + </dict> 1420 + </dict> 1421 + <dict> 1422 + <key>name</key> 1423 + <string>Markdown: Block Level</string> 1424 + <key>scope</key> 1425 + <string>meta.block-level.markdown</string> 1426 + <key>settings</key> 1427 + <dict> 1428 + <key>background</key> 1429 + <string>#F9F9F6</string> 1430 + </dict> 1431 + </dict> 1432 + <dict> 1433 + <key>name</key> 1434 + <string>Markdown: References</string> 1435 + <key>scope</key> 1436 + <string>meta.link.reference</string> 1437 + <key>settings</key> 1438 + <dict> 1439 + <key>background</key> 1440 + <string>#FFEBF9</string> 1441 + </dict> 1442 + </dict> 1443 + <dict> 1444 + <key>name</key> 1445 + <string>Matlab: Graphics Support</string> 1446 + <key>scope</key> 1447 + <string>support.graphics.matlab</string> 1448 + <key>settings</key> 1449 + <dict> 1450 + <key>background</key> 1451 + <string>#FFFDF0</string> 1452 + </dict> 1453 + </dict> 1454 + <dict> 1455 + <key>name</key> 1456 + <string>Matlab: Control Support</string> 1457 + <key>scope</key> 1458 + <string>support.function.control</string> 1459 + <key>settings</key> 1460 + <dict> 1461 + <key>background</key> 1462 + <string>#FFFFDB</string> 1463 + </dict> 1464 + </dict> 1465 + <dict> 1466 + <key>name</key> 1467 + <string>Mediawiki: Punctuation Fix Reminder</string> 1468 + <key>scope</key> 1469 + <string>punctuation.fix_this_later</string> 1470 + <key>settings</key> 1471 + <dict> 1472 + <key>foreground</key> 1473 + <string>#B20047</string> 1474 + </dict> 1475 + </dict> 1476 + <dict> 1477 + <key>name</key> 1478 + <string>Ocaml: Module</string> 1479 + <key>scope</key> 1480 + <string>meta.module</string> 1481 + <key>settings</key> 1482 + <dict> 1483 + <key>fontStyle</key> 1484 + <string>italic</string> 1485 + </dict> 1486 + </dict> 1487 + <dict> 1488 + <key>name</key> 1489 + <string>Ocaml: Module Reference</string> 1490 + <key>scope</key> 1491 + <string>meta.module-reference.ocaml</string> 1492 + <key>settings</key> 1493 + <dict> 1494 + <key>foreground</key> 1495 + <string>#933</string> 1496 + </dict> 1497 + </dict> 1498 + <dict> 1499 + <key>name</key> 1500 + <string>Ocaml: Action</string> 1501 + <key>scope</key> 1502 + <string>meta.action.ocaml</string> 1503 + <key>settings</key> 1504 + <dict> 1505 + <key>foreground</key> 1506 + <string>#933</string> 1507 + </dict> 1508 + </dict> 1509 + <dict> 1510 + <key>name</key> 1511 + <string>Php: Embedded Block</string> 1512 + <key>scope</key> 1513 + <string>source.php.embedded.block.html</string> 1514 + <key>settings</key> 1515 + <dict> 1516 + <key>background</key> 1517 + <string>#FFFFFA</string> 1518 + </dict> 1519 + </dict> 1520 + <dict> 1521 + <key>name</key> 1522 + <string>Php: Function Arguments</string> 1523 + <key>scope</key> 1524 + <string>meta.function.arguments</string> 1525 + <key>settings</key> 1526 + <dict> 1527 + <key>foreground</key> 1528 + <string>#26004C</string> 1529 + <key>fontStyle</key> 1530 + <string>italic</string> 1531 + </dict> 1532 + </dict> 1533 + <dict> 1534 + <key>name</key> 1535 + <string>Php: Object Function Call</string> 1536 + <key>scope</key> 1537 + <string>meta.function-call.object</string> 1538 + <key>settings</key> 1539 + <dict> 1540 + <key>foreground</key> 1541 + <string>#933</string> 1542 + </dict> 1543 + </dict> 1544 + <dict> 1545 + <key>name</key> 1546 + <string>Plain Text: </string> 1547 + <key>scope</key> 1548 + <string>meta.bullet-point.light, meta.bullet-point.star, meta.bullet-point.strong, punctuation.definition.item.text, meta.paragraph.text</string> 1549 + <key>settings</key> 1550 + <dict> 1551 + <key>foreground</key> 1552 + <string>#933</string> 1553 + </dict> 1554 + </dict> 1555 + <dict> 1556 + <key>name</key> 1557 + <string>Python: Language Variable </string> 1558 + <key>scope</key> 1559 + <string>variable.language.python</string> 1560 + <key>settings</key> 1561 + <dict> 1562 + <key>foreground</key> 1563 + <string>#61210B</string> 1564 + <key>fontStyle</key> 1565 + <string>italic</string> 1566 + </dict> 1567 + </dict> 1568 + <dict> 1569 + <key>name</key> 1570 + <string>Rd Documentation: </string> 1571 + <key>scope</key> 1572 + <string>punctuation.section.group.tex</string> 1573 + <key>settings</key> 1574 + <dict> 1575 + <key>foreground</key> 1576 + <string>#F00</string> 1577 + </dict> 1578 + </dict> 1579 + <dict> 1580 + <key>name</key> 1581 + <string>Regexp: Range</string> 1582 + <key>scope</key> 1583 + <string>constant.other.range.regexp</string> 1584 + <key>settings</key> 1585 + <dict> 1586 + <key>foreground</key> 1587 + <string>#00F</string> 1588 + </dict> 1589 + </dict> 1590 + <dict> 1591 + <key>name</key> 1592 + <string>Regexp: Anchor</string> 1593 + <key>scope</key> 1594 + <string>keyword.control.anchors.regexp</string> 1595 + <key>settings</key> 1596 + <dict> 1597 + <key>foreground</key> 1598 + <string>#690</string> 1599 + <key>fontStyle</key> 1600 + <string>bold</string> 1601 + </dict> 1602 + </dict> 1603 + <dict> 1604 + <key>name</key> 1605 + <string>Regexp: Set</string> 1606 + <key>scope</key> 1607 + <string>keyword.control.set.regexp</string> 1608 + <key>settings</key> 1609 + <dict> 1610 + <key>foreground</key> 1611 + <string>#909</string> 1612 + </dict> 1613 + </dict> 1614 + <dict> 1615 + <key>name</key> 1616 + <string>Regexp: Character Class</string> 1617 + <key>scope</key> 1618 + <string>keyword.other.backref-and-recursion.regexp</string> 1619 + <key>settings</key> 1620 + <dict> 1621 + <key>foreground</key> 1622 + <string>#630</string> 1623 + </dict> 1624 + </dict> 1625 + <dict> 1626 + <key>name</key> 1627 + <string>Regexp: Quantifier</string> 1628 + <key>scope</key> 1629 + <string>keyword.operator.quantifier.regexp</string> 1630 + <key>settings</key> 1631 + <dict> 1632 + <key>foreground</key> 1633 + <string>#099</string> 1634 + </dict> 1635 + </dict> 1636 + <dict> 1637 + <key>name</key> 1638 + <string>Restructured Text: Directive</string> 1639 + <key>scope</key> 1640 + <string>meta.other.directive.restructuredtext, punctuation.definition.raw.restructuredtext</string> 1641 + <key>settings</key> 1642 + <dict> 1643 + <key>foreground</key> 1644 + <string>#F00</string> 1645 + <key>fontStyle</key> 1646 + <string>bold</string> 1647 + </dict> 1648 + </dict> 1649 + <dict> 1650 + <key>name</key> 1651 + <string>Restructured Text: Commands</string> 1652 + <key>scope</key> 1653 + <string>markup.other.command.restructuredtext</string> 1654 + <key>settings</key> 1655 + <dict> 1656 + <key>background</key> 1657 + <string>#FFFFFC</string> 1658 + <key>foreground</key> 1659 + <string>#933</string> 1660 + <key>fontStyle</key> 1661 + <string>italic</string> 1662 + </dict> 1663 + </dict> 1664 + <dict> 1665 + <key>name</key> 1666 + <string>Restructured Text: Footnote</string> 1667 + <key>scope</key> 1668 + <string>meta.link.footnote</string> 1669 + <key>settings</key> 1670 + <dict> 1671 + <key>background</key> 1672 + <string>#FFE0FF</string> 1673 + <key>foreground</key> 1674 + <string>#933</string> 1675 + </dict> 1676 + </dict> 1677 + <dict> 1678 + <key>name</key> 1679 + <string>Restructured Text: Field</string> 1680 + <key>scope</key> 1681 + <string>punctuation.definition.field.restructuredtext</string> 1682 + <key>settings</key> 1683 + <dict> 1684 + <key>background</key> 1685 + <string>#933</string> 1686 + </dict> 1687 + </dict> 1688 + <dict> 1689 + <key>name</key> 1690 + <string>Restructured Text: Link</string> 1691 + <key>scope</key> 1692 + <string>punctuation.definition.link.restructuredtext</string> 1693 + <key>settings</key> 1694 + <dict> 1695 + <key>foreground</key> 1696 + <string>#0C9</string> 1697 + <key>fontStyle</key> 1698 + <string>bold</string> 1699 + </dict> 1700 + </dict> 1701 + <dict> 1702 + <key>name</key> 1703 + <string>Ruby: Readwrite</string> 1704 + <key>scope</key> 1705 + <string>variable.other.readwrite, punctuation.separator.method.ruby</string> 1706 + <key>settings</key> 1707 + <dict> 1708 + <key>foreground</key> 1709 + <string>#930</string> 1710 + <key>fontStyle</key> 1711 + <string>italic</string> 1712 + </dict> 1713 + </dict> 1714 + <dict> 1715 + <key>name</key> 1716 + <string>Ruby On Rails: Punctuation Separators</string> 1717 + <key>scope</key> 1718 + <string>punctuation.separator.key-value, punctuation.separator.variable</string> 1719 + <key>settings</key> 1720 + <dict> 1721 + <key>foreground</key> 1722 + <string>#FF5600</string> 1723 + </dict> 1724 + </dict> 1725 + <dict> 1726 + <key>name</key> 1727 + <string>Shell Unix Generic: Heredoc Unquoted</string> 1728 + <key>scope</key> 1729 + <string>string.unquoted.heredoc</string> 1730 + <key>settings</key> 1731 + <dict> 1732 + <key>foreground</key> 1733 + <string>#800000</string> 1734 + </dict> 1735 + </dict> 1736 + </array> 1737 + <key>uuid</key> 1738 + <string>3ca46127-0e70-4501-9a4d-52accf12a12d</string> 1739 + <key>colorSpaceName</key> 1740 + <string>sRGB</string> 1741 + <key>semanticClass</key> 1742 + <string>theme.light.wildlife</string> 1743 + </dict> 1744 + </plist>
+57
hooks/pre-commit
···
··· 1 + #!/usr/bin/env bash 2 + 3 + # Check if exiftool is installed 4 + if ! command -v exiftool &> /dev/null; then 5 + echo "Error: exiftool is not installed. Please install it." >&2 6 + exit 1 7 + fi 8 + 9 + # Flag to track if we found any draft files 10 + found_draft=0 11 + 12 + # First pass: check for draft files 13 + while read -r file; do 14 + case "$file" in 15 + *.md) 16 + # Check if file contains draft = true within +++ header section 17 + awk ' 18 + /^\+\+\+$/ { inblock = !inblock } 19 + inblock && /draft = true/ { found = 1 } 20 + END { exit(found ? 0 : 1) } 21 + ' "$file" 22 + if [ $? -eq 0 ]; then 23 + echo "Error: Draft file detected: $file" >&2 24 + echo "Please remove draft status or unstage this file before committing." >&2 25 + found_draft=1 26 + fi 27 + ;; 28 + *) 29 + ;; 30 + esac 31 + done < <(git diff --cached --name-only --diff-filter=ACMR) 32 + 33 + # Exit if we found any draft files 34 + if [ $found_draft -eq 1 ]; then 35 + exit 1 36 + fi 37 + 38 + # Second pass: process images 39 + while read -r file; do 40 + case "$file" in 41 + *.jpg|*.jpeg|*.png|*.gif|*.tiff|*.bmp) 42 + # Store output of exiftool command 43 + cleared_data=$(exiftool -all= --icc_profile:all -tagsfromfile @ -orientation -overwrite_original "$file") 44 + if [ $? -ne 0 ]; then 45 + echo "Error: exiftool failed to process $file" >&2 46 + exit 1 47 + fi 48 + echo "Cleared EXIF data for $file:" >&2 49 + echo "$cleared_data" >&2 50 + git add "$file" 51 + ;; 52 + *) 53 + ;; 54 + esac 55 + done < <(git diff --cached --name-only --diff-filter=ACMR) 56 + 57 + exit 0
+23
package.json
···
··· 1 + { 2 + "name": "zera", 3 + "type": "module", 4 + "scripts": { 5 + "dev": "bun run scripts/dev.ts", 6 + "serve": "bun run scripts/dev.ts", 7 + "build": "rm -rf .zola-build && bun run scripts/build.ts", 8 + "preprocess": "bun run scripts/preprocess.ts", 9 + "gen-og": "bun run scripts/genOG.ts" 10 + }, 11 + "dependencies": { 12 + "@types/cli-progress": "^3.11.6", 13 + "cli-progress": "^3.12.0", 14 + "dotenv": "^16.4.7", 15 + "glob": "^13.0.0", 16 + "sharp": "^0.34.5" 17 + }, 18 + "devDependencies": { 19 + "@types/bun": "latest", 20 + "puppeteer": "^23.6.0" 21 + } 22 + } 23 +
+159
sass/css/_copy-button.scss
···
··· 1 + i.icon { 2 + display: inline-block; 3 + mask-size: cover; 4 + background-color: var(--accent-text); 5 + width: 1rem; 6 + height: 1rem; 7 + font-style: normal; 8 + font-variant: normal; 9 + line-height: 0; 10 + text-rendering: auto; 11 + } 12 + 13 + .pre-container { 14 + --icon-copy: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' height='16' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 3c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3 0 .55-.45 1-1 1s-1-.45-1-1c0-.57-.43-1-1-1H3c-.57 0-1 .43-1 1v5c0 .57.43 1 1 1 .55 0 1 .45 1 1s-.45 1-1 1c-1.645 0-3-1.355-3-3zm5 5c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3v5c0 1.645-1.355 3-3 3H8c-1.645 0-3-1.355-3-3zm2 0v5c0 .57.43 1 1 1h5c.57 0 1-.43 1-1V8c0-.57-.43-1-1-1H8c-.57 0-1 .43-1 1m0 0'/%3E%3C/svg%3E"); 15 + --icon-done: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M7.883 0q-.486.008-.965.074a7.98 7.98 0 0 0-4.602 2.293 8.01 8.01 0 0 0-1.23 9.664 8.015 8.015 0 0 0 9.02 3.684 8 8 0 0 0 5.89-7.75 1 1 0 1 0-2 .008 5.986 5.986 0 0 1-4.418 5.816 5.996 5.996 0 0 1-6.762-2.766 5.99 5.99 0 0 1 .922-7.25 5.99 5.99 0 0 1 7.239-.984 1 1 0 0 0 1.363-.371c.273-.48.11-1.09-.371-1.367A8 8 0 0 0 9.492.14 8 8 0 0 0 7.882 0m7.15 1.998-.1.002a1 1 0 0 0-.687.34L7.95 9.535 5.707 7.29A1 1 0 0 0 4 8a1 1 0 0 0 .293.707l3 3c.195.195.465.3.742.293.277-.012.535-.133.719-.344l7-8A1 1 0 0 0 16 2.934a1 1 0 0 0-.34-.688 1 1 0 0 0-.627-.248'/%3E%3C/svg%3E"); 16 + 17 + background-color: var(--accent); 18 + padding: 0.4em; 19 + border-bottom: 5px solid var(--bg-light); 20 + border-radius: 7px 7px 10px 10px; 21 + 22 + .header { 23 + display: flex; 24 + justify-content: space-between; 25 + align-items: center; 26 + border-radius: 0.2em 0.2em 0 0; 27 + /* background-color: var(--accent); */ 28 + background-size: 200%; 29 + padding: 0.25rem; 30 + height: 2.5rem; 31 + 32 + span { 33 + margin-inline-start: 0.75rem; 34 + color: var(--purple-gray); 35 + font-weight: bold; 36 + line-height: 1; 37 + } 38 + 39 + button { 40 + appearance: none; 41 + transition: 200ms; 42 + cursor: pointer; 43 + border: none; 44 + border-radius: 0.4rem; 45 + background-color: transparent; 46 + padding: 0.5rem; 47 + color: var(--purple-gray); 48 + line-height: 0; 49 + 50 + &:hover { 51 + background-color: color-mix( 52 + in oklab, 53 + var(--accent) 80%, 54 + var(--purple-gray) 55 + ); 56 + } 57 + 58 + &:focus { 59 + background-color: color-mix( 60 + in oklab, 61 + var(--accent) 80%, 62 + var(--purple-gray) 63 + ); 64 + } 65 + 66 + &:active { 67 + transform: scale(0.9); 68 + } 69 + 70 + &:disabled { 71 + cursor: not-allowed; 72 + 73 + &:active { 74 + transform: none; 75 + } 76 + } 77 + 78 + .icon { 79 + -webkit-mask-image: var(--icon-copy); 80 + mask-image: var(--icon-copy); 81 + transition: 200ms; 82 + 83 + :root[dir*="rtl"] & { 84 + transform: scaleX(-1); 85 + } 86 + } 87 + } 88 + 89 + &.active { 90 + button { 91 + animation: active-copy 0.3s; 92 + 93 + color: var(--purple-gray); 94 + 95 + .icon { 96 + -webkit-mask-image: var(--icon-done); 97 + mask-image: var(--icon-done); 98 + } 99 + } 100 + 101 + @keyframes active-copy { 102 + 50% { 103 + transform: scale(0.9); 104 + } 105 + 100% { 106 + transform: none; 107 + } 108 + } 109 + } 110 + } 111 + 112 + pre { 113 + margin: 0; 114 + box-shadow: none; 115 + border-radius: 0.3rem; 116 + border: none; 117 + } 118 + } 119 + 120 + blockquote:not(.pre-container blockquote):has(+ pre) { 121 + margin-bottom: 0; 122 + background-color: var(--accent); 123 + border-radius: 7px 7px 0 0; 124 + padding: 0.25rem; 125 + display: flex; 126 + justify-content: space-between; 127 + align-items: center; 128 + 129 + p { 130 + margin: 0; 131 + padding: 0.25rem 0.75rem; 132 + font-weight: bold; 133 + line-height: 1; 134 + } 135 + } 136 + 137 + pre.z-code:not(.pre-container pre) { 138 + position: relative; 139 + border: none; 140 + background-color: var(--accent); 141 + padding: 0.4em; 142 + border-bottom: 5px solid var(--bg-light); 143 + border-radius: 7px 7px 10px 10px; 144 + 145 + code { 146 + border-radius: 0.3rem; 147 + display: block; 148 + overflow-x: auto; 149 + padding: 1em; 150 + background-color: var(--nightshade-violet); 151 + margin: 0; 152 + border: none; 153 + box-shadow: none; 154 + } 155 + } 156 + 157 + blockquote:not(.pre-container blockquote) + pre.z-code { 158 + border-radius: 0 0 7px 7px; 159 + }
+32
sass/css/_emoji-inline.scss
···
··· 1 + .emoji-inline--wrapper { 2 + vertical-align: baseline; 3 + height: auto; 4 + position: relative; 5 + overflow: visible; 6 + vertical-align: top; 7 + object-fit: contain; 8 + align-items: center; 9 + display: inline-flex; 10 + overflow: hidden; 11 + width: 1.375rem; 12 + height: 1.375rem; 13 + } 14 + 15 + .emoji-inline--wrapper img { 16 + object-fit: contain; 17 + position: absolute; 18 + top: 50%; 19 + overflow: hidden; 20 + width: 1.375rem !important; 21 + height: 1.375rem !important; 22 + margin-top: -0.6875rem; 23 + margin-left: 0 !important; 24 + margin-right: 0 !important; 25 + margin-bottom: 0 !important; 26 + border: none !important; 27 + border-radius: 0 !important; 28 + padding: 0 !important; 29 + opacity: 1 !important; 30 + max-width: 1.375rem !important; 31 + display: inline !important; 32 + }
+109
sass/css/_lightbox.scss
···
··· 1 + #lightbox { 2 + display: none; 3 + position: fixed; 4 + top: 0; 5 + left: 0; 6 + width: 100%; 7 + height: 100%; 8 + background-color: rgba(0, 0, 0, 0.8); 9 + z-index: 9999; 10 + justify-content: center; 11 + align-items: center; 12 + } 13 + 14 + .lightbox-content { 15 + position: relative; 16 + max-width: 90%; 17 + max-height: 90%; 18 + display: flex; 19 + flex-direction: column; 20 + align-items: center; 21 + justify-content: center; 22 + } 23 + 24 + #lightbox-img { 25 + max-width: 100%; 26 + max-height: 80vh; 27 + object-fit: contain; 28 + border: none; 29 + padding: 0; 30 + margin: 0; 31 + background: transparent; 32 + border-radius: 0; 33 + } 34 + 35 + .lightbox-controls { 36 + display: flex; 37 + gap: 2rem; 38 + margin-top: 1rem; 39 + align-items: center; 40 + } 41 + 42 + .lightbox-close { 43 + position: fixed; 44 + top: 20px; 45 + right: 20px; 46 + font-size: 40px; 47 + color: var(--text); 48 + background: transparent !important; 49 + border: none; 50 + cursor: pointer; 51 + padding: 0; 52 + line-height: 1; 53 + -webkit-tap-highlight-color: transparent; 54 + transition: color 120ms ease, transform 300ms ease; 55 + } 56 + 57 + .lightbox-close:hover { 58 + background: transparent !important; 59 + color: var(--accent); 60 + background-color: transparent !important; 61 + transform: rotate(90deg); 62 + } 63 + 64 + .lightbox-close:focus { 65 + background: transparent !important; 66 + background-color: transparent !important; 67 + } 68 + 69 + .lightbox-prev, 70 + .lightbox-next { 71 + font-size: 30px; 72 + color: var(--text); 73 + background: transparent !important; 74 + border: none; 75 + cursor: pointer; 76 + padding: 0.5rem 1rem; 77 + user-select: none; 78 + -webkit-tap-highlight-color: transparent; 79 + transition: color 120ms ease; 80 + } 81 + 82 + .lightbox-prev:hover, 83 + .lightbox-next:hover { 84 + background: transparent !important; 85 + color: var(--accent); 86 + background-color: transparent !important; 87 + } 88 + 89 + .lightbox-prev:focus, 90 + .lightbox-next:focus { 91 + background: transparent !important; 92 + background-color: transparent !important; 93 + } 94 + 95 + @media only screen and (max-width: 720px) { 96 + .lightbox-close { 97 + top: 10px; 98 + right: 10px; 99 + } 100 + } 101 + 102 + .img-container { 103 + cursor: pointer; 104 + transition: opacity 120ms ease; 105 + } 106 + 107 + .img-container:hover { 108 + opacity: 0.9; 109 + }
+17
sass/css/_theme-toggle.scss
···
··· 1 + #theme-toggle-label i.icon { 2 + --icon-dark: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z'/%3E%3C/svg%3E"); 3 + 4 + --icon-light: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z'/%3E%3C/svg%3E"); 5 + 6 + --icon-toggle: var(--icon-light); 7 + 8 + -webkit-mask-image: var(--icon-toggle); 9 + mask-image: var(--icon-toggle); 10 + background-color: var(--accent); 11 + color: var(--accent); 12 + 13 + width: 1.2rem; 14 + height: 1.2rem; 15 + transform: translateY(0.25rem); 16 + overflow: visible; 17 + }
+3
sass/css/_view-transitions.scss
···
··· 1 + @view-transition { 2 + navigation: auto; 3 + }
-4
sass/css/main.sass
··· 1 - @import "reset" 2 - @import "suCSS" 3 - @import "syntax-theme" 4 - @import "mods"
···
+10
sass/css/main.scss
···
··· 1 + @use "reset"; 2 + 3 + @use "suCSS"; 4 + @use "syntax-theme"; 5 + @use "mods"; 6 + 7 + @use "copy-button"; 8 + @use "view-transitions"; 9 + @use "emoji-inline"; 10 + @use "lightbox";
+377 -83
sass/css/mods.css
··· 1 #nav-bar { 2 - padding: 0.625rem 0 0 0; 3 - display: flex; 4 - flex-direction: row; 5 - gap: 0.25rem; 6 - flex-wrap: wrap; 7 - justify-content: flex-end; 8 - align-items: center; 9 - align-content: flex-end; 10 } 11 12 #footer-container { 13 - display: flex; 14 - flex-wrap: wrap; 15 - justify-content: center; 16 - align-items: center; 17 - text-align: center; 18 - padding-bottom: 0.5rem; 19 } 20 21 - .accent-data { 22 - color: var(--accent-light); 23 } 24 25 - .theme-transition { 26 - transition: background-color 0.3s ease; 27 } 28 29 .tags-data { 30 - display: flex; 31 - flex-direction: row; 32 - flex-wrap: wrap; 33 - justify-content: flex-end; 34 - align-items: flex-start; 35 - align-content: flex-end; 36 - gap: 0.25rem; 37 } 38 39 .title-list li { 40 - margin-bottom: 0.375rem; 41 } 42 43 /* icons settings */ 44 .icons { 45 - width: 1.3rem; 46 - height: 1.3rem; 47 - aspect-ratio: 1 / 1; 48 - display: inline-block; 49 - vertical-align: middle; 50 - color: var(--text); 51 - fill: var(--text); 52 - background-color: transparent; 53 - cursor: pointer; 54 } 55 56 .icons:hover { 57 - background-color: transparent; 58 - color: var(--accent); 59 } 60 61 /* footnotes */ 62 .footnote-definition { 63 - margin: 0 0 0 0.125rem; 64 } 65 66 .footnote-definition-label { 67 - color: var(--accent); 68 } 69 70 .footnote-definition p { 71 - display: inline; 72 - margin: 0.625rem 0 0 0.625rem; 73 } 74 75 /* general classes */ 76 .no-style { 77 - padding: 0; 78 - margin: 0; 79 - border: none; 80 - border-radius: 0; 81 } 82 83 .no-style:hover { 84 - background-color: transparent; 85 - color: var(--accent); 86 } 87 88 .center { 89 - text-align: center; 90 } 91 92 - .center img { 93 - display: block; 94 - margin: 1rem auto; 95 } 96 97 .center figcaption { 98 - text-align: center; 99 } 100 101 .float-right { 102 - float: right; 103 } 104 105 .float-left { 106 - float: left; 107 } 108 109 - div code, 110 li code, 111 p code { 112 - color: var(--text); 113 - background-color: var(--bg-light); 114 } 115 116 pre { 117 - border-top-left-radius: 0; 118 } 119 120 blockquote { 121 - padding-top: 0.2rem; 122 - padding-bottom: 0.2rem; 123 } 124 125 blockquote:has(+ pre) { 126 - display: inline-block; 127 - border: none; 128 - font-family: "code" !important; 129 - font-size: 0.8rem; 130 - font-weight: 600; 131 - margin: 0; 132 - margin-bottom: 0.2rem; 133 - margin-block: 0 0; 134 - border-top-left-radius: 5px; 135 - border-top-right-radius: 5px; 136 - padding-left: 0.75rem; 137 - padding-right: 0.75rem; 138 - padding-top: 0.25rem; 139 - padding-bottom: 0.25rem; 140 - position: relative; 141 - background-color: var(--accent); 142 } 143 144 blockquote:has(+ pre) p { 145 - margin: 0; 146 - color: var(--accent-text); 147 } 148 149 .yt-embed { 150 - width: 100%; 151 - display: flex; 152 - justify-content: center; 153 } 154 155 .yt-embed iframe { 156 - width: 100%; 157 - aspect-ratio: 16 / 9; 158 }
··· 1 + header { 2 + grid-column: 1 / -1; 3 + padding: 0.625rem 1rem 0 1rem; 4 + } 5 + 6 + #header-container { 7 + display: flex; 8 + flex-direction: row; 9 + justify-content: space-between; 10 + align-items: center; 11 + } 12 + 13 + #now-playing:not(:empty) { 14 + font-size: 0.85rem; 15 + color: var(--text-light); 16 + display: flex; 17 + flex-direction: column; 18 + gap: 0.15rem; 19 + flex: 1; 20 + min-width: 0; 21 + } 22 + 23 + #now-playing .now-playing-line { 24 + display: flex; 25 + align-items: center; 26 + gap: 0.25rem; 27 + min-width: 0; 28 + } 29 + 30 + #now-playing .track-name { 31 + overflow: hidden; 32 + text-overflow: ellipsis; 33 + white-space: nowrap; 34 + min-width: 0; 35 + } 36 + 37 + #now-playing .artist-line { 38 + overflow: hidden; 39 + text-overflow: ellipsis; 40 + white-space: nowrap; 41 + } 42 + 43 + #now-playing a { 44 + color: var(--link); 45 + text-decoration: none; 46 + min-width: 0; 47 + overflow: hidden; 48 + } 49 + 50 + #now-playing a:hover { 51 + text-decoration: underline; 52 + } 53 + 54 + /* Hide music widget on very small screens */ 55 + @media (max-width: 640px) { 56 + #now-playing { 57 + display: none !important; 58 + } 59 + } 60 + 61 #nav-bar { 62 + display: flex; 63 + flex-direction: row; 64 + flex-wrap: wrap; 65 + align-items: center; 66 + } 67 + 68 + #nav-bar a { 69 + text-decoration: none; 70 + color: var(--link); 71 + padding: 0 0.25rem; 72 + border-radius: 0.25rem; 73 + transition: all 120ms ease; 74 + position: relative; 75 + font-weight: 600; 76 + } 77 + 78 + #nav-bar a:hover { 79 + color: var(--accent); 80 + background-color: color-mix(in oklab, var(--accent) 15%, transparent); 81 + } 82 + 83 + #nav-bar a.active { 84 + color: var(--link-visited); 85 + background-color: color-mix(in oklab, var(--accent) 20%, transparent); 86 + margin: 0 0.15rem; 87 } 88 89 #footer-container { 90 + display: flex; 91 + flex-direction: column; 92 + justify-content: center; 93 + align-items: center; 94 + text-align: center; 95 + padding-bottom: 0.5rem; 96 } 97 98 + #footer-container p { 99 + margin: 0; 100 } 101 102 + .accent-data { 103 + color: var(--accent-dark); 104 } 105 106 .tags-data { 107 + display: flex; 108 + flex-direction: row; 109 + flex-wrap: wrap; 110 + justify-content: flex-end; 111 + align-items: flex-start; 112 + align-content: flex-end; 113 + gap: 0.25rem; 114 } 115 116 .title-list li { 117 + margin-bottom: 0.375rem; 118 } 119 120 /* icons settings */ 121 .icons { 122 + width: 1.3rem; 123 + height: 1.3rem; 124 + aspect-ratio: 1 / 1; 125 + display: inline-block; 126 + vertical-align: middle; 127 + color: var(--text); 128 + fill: var(--text); 129 + background-color: transparent; 130 + cursor: pointer; 131 } 132 133 .icons:hover { 134 + background-color: transparent; 135 + color: var(--accent); 136 } 137 138 /* footnotes */ 139 .footnote-definition { 140 + margin: 0 0 0 0.125rem; 141 } 142 143 .footnote-definition-label { 144 + color: var(--accent); 145 } 146 147 .footnote-definition p { 148 + display: inline; 149 + margin: 0.625rem 0 0 0.625rem; 150 } 151 152 /* general classes */ 153 .no-style { 154 + padding: 0; 155 + margin: 0; 156 + border: none; 157 + border-radius: 0; 158 } 159 160 .no-style:hover { 161 + background-color: transparent; 162 + color: var(--accent); 163 } 164 165 .center { 166 + text-align: center; 167 } 168 169 + .center .img-container { 170 + margin: 1rem auto; 171 } 172 173 .center figcaption { 174 + text-align: center; 175 } 176 177 .float-right { 178 + float: right; 179 } 180 181 .float-left { 182 + float: left; 183 } 184 185 + div>code, 186 li code, 187 p code { 188 + padding: 0.1em 0.3em 0.1em 0.3em; 189 + color: var(--text-dark); 190 + background-color: var(--bg-light); 191 } 192 193 pre { 194 + border-top-left-radius: 0; 195 } 196 197 blockquote { 198 + padding-top: 0.2rem; 199 + padding-bottom: 0.2rem; 200 } 201 202 blockquote:has(+ pre) { 203 + display: inline-block; 204 + border: none; 205 + font-family: "code", monospace !important; 206 + font-size: 0.8rem; 207 + font-weight: 600; 208 + margin: 0; 209 + margin-bottom: 0.2rem; 210 + margin-block: 0 0; 211 + border-top-left-radius: 0.2em; 212 + border-top-right-radius: 0.2em; 213 + padding-left: 0.75rem; 214 + padding-right: 0.75rem; 215 + padding-top: 0.25rem; 216 + padding-bottom: 0.25rem; 217 + position: relative; 218 + background-color: var(--accent); 219 } 220 221 blockquote:has(+ pre) p { 222 + margin: 0; 223 + color: var(--accent-text); 224 + } 225 + 226 + blockquote:has(+ pre) p::selection { 227 + background: var(--pink-puree); 228 } 229 230 .yt-embed { 231 + width: 100%; 232 + display: flex; 233 + flex-direction: column; 234 + justify-content: center; 235 + align-content: center; 236 + text-align: center; 237 } 238 239 .yt-embed iframe { 240 + width: 100%; 241 + aspect-ratio: 16 / 9; 242 + align-self: center; 243 + } 244 + 245 + .yt-embed figcaption { 246 + margin-top: 1rem; 247 + text-align: center; 248 + } 249 + 250 + #elr table { 251 + border-style: none; 252 + } 253 + 254 + #elr td { 255 + border-style: dashed; 256 + } 257 + 258 + img.avatar { 259 + width: 24px; 260 + height: 24px; 261 + aspect-ratio: 1 / 1; 262 + border-radius: 50%; 263 + vertical-align: middle; 264 + display: inline-block; 265 + border: none; 266 + padding: 0; 267 + margin: 0; 268 + } 269 + 270 + cite { 271 + display: inline-flex; 272 + align-items: center; 273 + vertical-align: middle; 274 + } 275 + 276 + cite a { 277 + display: inline-flex; 278 + align-items: center; 279 + } 280 + 281 + cite a img.avatar { 282 + margin-right: 5px; 283 + } 284 + 285 + .image-gallery { 286 + display: flex; 287 + flex-direction: column; 288 + gap: 0.5rem; 289 + padding-top: 0.5rem; 290 + } 291 + 292 + .image-gallery img { 293 + max-width: 100%; 294 + border-radius: 0.25rem; 295 + padding: 0; 296 + margin: 0; 297 + } 298 + 299 + .side-by-side { 300 + display: flex; 301 + flex-direction: row; 302 + } 303 + 304 + .side-by-side img { 305 + max-width: calc(50% - 0.25rem); 306 + } 307 + 308 + .gallery-grid { 309 + display: grid; 310 + grid-template-columns: repeat(2, 1fr); 311 + gap: 0.1rem; 312 } 313 + 314 + .gallery-grid img { 315 + width: 100%; 316 + height: auto; 317 + object-fit: cover; 318 + } 319 + 320 + .inlined-bubbles { 321 + width: auto; 322 + pointer-events: none; 323 + display: block; 324 + margin-top: 10px; 325 + text-align: center; 326 + } 327 + 328 + .bubble { 329 + margin-top: 1rem; 330 + padding: 0.7em 1.2em 0.7em 1.2em; 331 + background: var(--accent); 332 + border-bottom: 5px solid var(--bg-light); 333 + border-radius: 7px 7px 10px 10px; 334 + font-size: 1rem; 335 + font-weight: bold; 336 + color: var(--accent-text); 337 + display: inline-block; 338 + text-align: center; 339 + transition: 340 + transform 0.3s ease, 341 + opacity 0.3s ease; 342 + } 343 + 344 + @keyframes bubbleIn { 345 + 0% { 346 + opacity: 0; 347 + transform: translateY(10px) scale(0.95); 348 + } 349 + 350 + 100% { 351 + opacity: 1; 352 + transform: translateY(0) scale(1); 353 + } 354 + } 355 + 356 + @media (prefers-reduced-motion: no-preference) { 357 + .bubble.animate-in { 358 + animation: bubbleIn 0.3s ease-out forwards; 359 + } 360 + } 361 + 362 + .bubble a { 363 + color: var(--accent-text); 364 + text-decoration: none; 365 + } 366 + 367 + 368 + 369 + .bubble > span { 370 + display: flex; 371 + flex-wrap: wrap; 372 + justify-content: center; 373 + gap: 0 0.3em; 374 + } 375 + 376 + #time-ago-wrap { 377 + white-space: nowrap; 378 + } 379 + 380 + #time-ago-wrap.wrapped .time-dash { 381 + display: none; 382 + } 383 + 384 + #time-ago { 385 + font-weight: normal; 386 + font-size: 0.8rem; 387 + } 388 + 389 + .badge-row { 390 + display: flex; 391 + flex-wrap: wrap; 392 + justify-content: center; 393 + gap: 8px; 394 + padding-bottom: 0.5rem; 395 + } 396 + 397 + .badge-row img { 398 + display: inline-block; 399 + border: none; 400 + border-radius: 0; 401 + box-shadow: none; 402 + max-width: 100%; 403 + height: auto; 404 + margin: 0; 405 + padding: 0; 406 + } 407 + 408 + .badge-row a { 409 + display: inline-flex; 410 + align-items: center; 411 + } 412 + 413 + .img-group { 414 + display: flex; 415 + flex-direction: row; 416 + gap: 1rem; 417 + max-width: 100%; 418 + justify-content: center; 419 + align-items: flex-start; 420 + } 421 + 422 + .img-group .img-container { 423 + background-color: var(--accent); 424 + border-bottom: 4px solid var(--bg-light); 425 + border-radius: 7px 7px 10px 10px; 426 + padding: 0.35rem; 427 + margin: 1rem 0; 428 + line-height: 0; 429 + } 430 + 431 + .img-group img { 432 + max-width: 100%; 433 + height: auto; 434 + border-radius: 0.35rem; 435 + } 436 + 437 + :root { 438 + --nightshade-violet: oklch(0.27 0.0242 287.67); 439 + --purple-night: oklch(27.58% 0.0203 289.13); 440 + --red-crushed-grape: oklch(0.6829 0.1189 296.74); 441 + --dark-crushed-grape: oklch(0.6261 0.1289 284.99); 442 + --light-crushed-grape: oklch(0.7727 0.094 296.74); 443 + --reseda-green: oklch(62.33% 0.0475 126.94); 444 + --earth-yellow: oklch(86.49% 0.018 73.05); 445 + --sunset: oklch(0.86 0.0213 73.05); 446 + --ultra-violet: oklch(42.21% 0.0676 297.45); 447 + --rose-quartz: oklch(65.32% 0.0585 311.96); 448 + --pink-puree: oklch(75.65% 0.0555 290.76); 449 + --lavendar-breeze: oklch(91.06% 0.0223 290.76); 450 + --purple-gray: oklch(25.63% 0.0002 290.76); 451 + --alice-blue: oklch(95.38% 0.0118 239.91); 452 + }
+24 -24
sass/css/reset.css
··· 2 *, 3 *::before, 4 *::after { 5 - box-sizing: border-box; 6 - -webkit-box-sizing: border-box; 7 } 8 9 * { 10 - margin: 0; 11 } 12 13 /* Prevent font size inflation */ 14 html { 15 - -moz-text-size-adjust: none; 16 - -webkit-text-size-adjust: none; 17 - text-size-adjust: none; 18 } 19 20 /* Remove default margin in favour of better control in authored CSS */ ··· 28 blockquote, 29 dl, 30 dd { 31 - margin-block-end: 0; 32 } 33 34 /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ ··· 36 ol, 37 ul[role="list"], 38 ol[role="list"] { 39 - list-style: none; 40 } 41 42 /* Set core body defaults */ 43 body { 44 - min-height: 100vh; 45 - line-height: 1.5; 46 - -webkit-font-smoothing: antialiased; 47 } 48 49 /* Set shorter line heights on headings and interactive elements */ ··· 54 button, 55 input, 56 label { 57 - line-height: 1.1; 58 } 59 60 /* Balance text wrapping on headings */ ··· 62 h2, 63 h3, 64 h4 { 65 - text-wrap: balance; 66 } 67 68 p, ··· 72 h4, 73 h5, 74 h6 { 75 - overflow-wrap: break-word; 76 } 77 78 /* A elements that don't have a class get default styles */ 79 a:not([class]) { 80 - text-decoration-skip-ink: auto; 81 } 82 83 /* Make images easier to work with */ ··· 86 video, 87 canvas, 88 svg { 89 - max-width: 100%; 90 - display: block; 91 } 92 93 /* Inherit fonts for inputs and buttons */ ··· 96 textarea, 97 select, 98 progress { 99 - appearance: none; 100 - -webkit-appearance: none; 101 - -moz-appearance: none; 102 - font: inherit; 103 } 104 105 /* Make sure textareas without a rows attribute are not tiny */ 106 textarea:not([rows]) { 107 - min-height: 10em; 108 } 109 110 /* Anything that has been anchored to should have extra scroll margin */ 111 :target { 112 - scroll-margin-block: 5ex; 113 } 114 115 #root, 116 #__next { 117 - isolation: isolate; 118 }
··· 2 *, 3 *::before, 4 *::after { 5 + box-sizing: border-box; 6 + -webkit-box-sizing: border-box; 7 } 8 9 * { 10 + margin: 0; 11 } 12 13 /* Prevent font size inflation */ 14 html { 15 + -moz-text-size-adjust: none; 16 + -webkit-text-size-adjust: none; 17 + text-size-adjust: none; 18 } 19 20 /* Remove default margin in favour of better control in authored CSS */ ··· 28 blockquote, 29 dl, 30 dd { 31 + margin-block-end: 0; 32 } 33 34 /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ ··· 36 ol, 37 ul[role="list"], 38 ol[role="list"] { 39 + list-style: none; 40 } 41 42 /* Set core body defaults */ 43 body { 44 + min-height: 100vh; 45 + line-height: 1.5; 46 + -webkit-font-smoothing: antialiased; 47 } 48 49 /* Set shorter line heights on headings and interactive elements */ ··· 54 button, 55 input, 56 label { 57 + line-height: 1.1; 58 } 59 60 /* Balance text wrapping on headings */ ··· 62 h2, 63 h3, 64 h4 { 65 + text-wrap: balance; 66 } 67 68 p, ··· 72 h4, 73 h5, 74 h6 { 75 + overflow-wrap: break-word; 76 } 77 78 /* A elements that don't have a class get default styles */ 79 a:not([class]) { 80 + text-decoration-skip-ink: auto; 81 } 82 83 /* Make images easier to work with */ ··· 86 video, 87 canvas, 88 svg { 89 + max-width: 100%; 90 + display: block; 91 } 92 93 /* Inherit fonts for inputs and buttons */ ··· 96 textarea, 97 select, 98 progress { 99 + appearance: none; 100 + -webkit-appearance: none; 101 + -moz-appearance: none; 102 + font: inherit; 103 } 104 105 /* Make sure textareas without a rows attribute are not tiny */ 106 textarea:not([rows]) { 107 + min-height: 10em; 108 } 109 110 /* Anything that has been anchored to should have extra scroll margin */ 111 :target { 112 + scroll-margin-block: 5ex; 113 } 114 115 #root, 116 #__next { 117 + isolation: isolate; 118 }
+416 -359
sass/css/suCSS.css
··· 1 :root, 2 ::backdrop { 3 - /* set sans-serif & mono fonts */ 4 - --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, 5 - "Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica, 6 - "Helvetica Neue", sans-serif; 7 - --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 8 - --standard-border-radius: 5px; 9 10 - /* default colors */ 11 - --bg: #eeeeee; 12 - --bg-light: #cbcdcd; 13 - --text: #41474e; 14 - --text-light: #686764; 15 - --accent: #58310ac5; 16 - --accent-light: #e08f67; 17 - --accent-text: var(--bg); 18 - --border: #646868; 19 - --link: #573819c5; 20 - } 21 - 22 - /* theme media queries */ 23 - @media (prefers-color-scheme: dark) { 24 - :root, 25 - ::backdrop { 26 - color-scheme: dark; 27 - --bg: #222529; 28 - --bg-light: #464949; 29 - --text: #d6d6d6; 30 - --text-light: #c5c0b7; 31 - --accent: #78b6ad; 32 - --accent-light: #87c9e5; 33 - --accent-text: var(--bg); 34 - --border: #dbd5bc; 35 - --link: #e2c8a2; 36 - } 37 - img, 38 - video { 39 - opacity: 0.8; 40 - } 41 - } 42 - 43 - @media (prefers-color-scheme: light) { 44 - :root, 45 - ::backdrop { 46 - color-scheme: light; 47 - --bg: #eeeeee; 48 - --bg-light: #cbcdcd; 49 - --text: #41474e; 50 - --text-light: #686764; 51 - --accent: #58310ac5; 52 - --accent-light: #e08f67; 53 - --accent-text: var(--bg); 54 - --border: #646868; 55 - --link: #573819c5; 56 - } 57 - } 58 - 59 - [data-theme="light"] { 60 - /* default (light) theme */ 61 - color-scheme: light; 62 - --bg: #eeeeee; 63 - --bg-light: #cbcdcd; 64 - --text: #41474e; 65 - --text-light: #686764; 66 - --accent: #58310ac5; 67 - --accent-light: #56412bc5; 68 - --accent-text: var(--bg); 69 - --border: #646868; 70 - --link: #573819c5; 71 - } 72 - 73 - [data-theme="dark"] { 74 - color-scheme: dark; 75 - --bg: #222529; 76 - --bg-light: #464949; 77 - --text: #d6d6d6; 78 - --text-light: #c5c0b7; 79 - --accent: #78b4b6e3; 80 - --accent-light: #c5edefe6; 81 - --accent-text: var(--bg); 82 - --border: #dbd5bc; 83 - --link: #e2c8a2; 84 } 85 86 ::selection, 87 ::-moz-selection { 88 - color: var(--bg); 89 - background: var(--accent); 90 } 91 92 /* chromium scrollbars */ 93 ::-webkit-scrollbar { 94 - width: 8px; 95 - height: 8px; 96 - overflow: visible; 97 } 98 ::-webkit-scrollbar-thumb { 99 - background: var(--accent); 100 - width: 12px; 101 } 102 ::-webkit-scrollbar-track { 103 - background: var(--bg-light); 104 } 105 106 /* firefox scrollbars */ 107 * { 108 - scrollbar-color: var(--accent) var(--bg-light); 109 - scrollbar-width: thin; 110 - scrollbar-height: thin; 111 } 112 113 html { 114 - color-scheme: light dark; 115 - font-family: var(--mono-font); 116 - scroll-behavior: smooth; 117 } 118 119 body { 120 - min-height: 100svh; 121 - color: var(--text); 122 - background-color: var(--bg); 123 - font-size: 1rem; 124 - display: grid; 125 - grid-template-columns: 1fr min(45rem, 90%) 1fr; 126 - grid-template-rows: auto 1fr auto; 127 - grid-row-gap: 0.625rem; 128 } 129 - body > * { 130 - grid-column: 2; 131 } 132 133 - body > footer { 134 - color: var(--text-light); 135 - font-size: 0.875; 136 } 137 138 /* Format headers */ 139 h1 { 140 - font-size: 2rem; 141 } 142 h2 { 143 - font-size: 1.75rem; 144 } 145 h3 { 146 - font-size: 1.5rem; 147 } 148 h4 { 149 - font-size: 1.25rem; 150 } 151 h5 { 152 - font-size: 1rem; 153 } 154 h6 { 155 - font-size: 0.75rem; 156 } 157 158 h1, ··· 161 h4, 162 h5, 163 h6 { 164 - margin: 0.5em 0 0.5em 0; 165 } 166 167 /* Fix line height when title wraps */ 168 h1, 169 h2, 170 h3 { 171 - line-height: 1.1; 172 - } 173 - 174 - h1::before, 175 - h2::before, 176 - h3::before, 177 - h4::before, 178 - h5::before, 179 - h6::before { 180 - color: var(--accent); 181 - content: "# "; 182 } 183 184 @media only screen and (max-width: 720px) { 185 - h1 { 186 - font-size: 1.5rem; 187 - } 188 - h2 { 189 - font-size: 1.25rem; 190 - } 191 - h3 { 192 - font-size: 1rem; 193 - } 194 - h4 { 195 - font-size: 0.75rem; 196 - } 197 - h5 { 198 - font-size: 0.5rem; 199 - } 200 - h6 { 201 - font-size: 0.25rem; 202 - } 203 } 204 205 p { 206 - margin: 1rem 0; 207 } 208 209 /* format links */ 210 - a, 211 a:visited { 212 - text-decoration: none; 213 - font-weight: bold; 214 - font-style: italic; 215 - border-radius: 0.125rem; 216 - color: var(--link); 217 } 218 219 - a:hover { 220 - background-color: var(--link); 221 - color: var(--bg); 222 } 223 224 /* format lists */ 225 ul { 226 - list-style: none; 227 - margin-top: 0.25rem; 228 - margin-bottom: 0.25rem; 229 } 230 231 ol { 232 - list-style-type: decimal; 233 - margin-top: 0.25rem; 234 - margin-bottom: 0.25rem; 235 } 236 237 li { 238 - margin-bottom: 0.125rem; 239 } 240 241 ul li::marker { 242 - content: "ยป "; 243 - color: var(--accent); 244 - } 245 - 246 - ul li:hover::marker { 247 - content: "# "; 248 - font-weight: 700; 249 - color: var(--link); 250 } 251 252 ol li::marker { 253 - color: var(--accent); 254 } 255 256 ol li:hover::marker { 257 - font-weight: 700; 258 - color: var(--link); 259 } 260 261 /* Use flexbox to allow items to wrap, as needed */ 262 - header > nav ul, 263 - header > nav ol { 264 - display: flex; 265 - flex-direction: row; 266 - flex-wrap: wrap; 267 - align-content: space-around; 268 - justify-content: right; 269 - list-style-type: none; 270 - margin: 0.5rem 0 0 0; 271 - padding: 0; 272 - gap: 1rem; 273 } 274 275 /* List items are inline elements, make them behave more like blocks */ 276 - header > nav ul li, 277 - header > nav ol li { 278 - display: inline-block; 279 } 280 281 /* Consolidate box styling */ 282 aside, 283 details, 284 progress { 285 - background-color: var(--bg-light); 286 - border-radius: var(--standard-border-radius); 287 } 288 289 aside { 290 - font-size: 1rem; 291 - width: 35%; 292 - padding: 0 10px; 293 - margin-inline-start: 10px; 294 - float: right; 295 } 296 *[dir="rtl"] aside { 297 - float: left; 298 } 299 300 /* make aside full-width on mobile */ 301 @media only screen and (max-width: 720px) { 302 - aside { 303 - width: 100%; 304 - float: none; 305 - margin-inline-start: 0; 306 - } 307 } 308 309 details { 310 - padding: 0.5rem; 311 } 312 313 summary { 314 - cursor: pointer; 315 - font-weight: bold; 316 - word-break: break-all; 317 } 318 319 - details[open] > summary + * { 320 - margin-top: 0; 321 } 322 323 - details[open] > summary { 324 - margin-bottom: 0.5rem; 325 } 326 327 - details[open] > :last-child { 328 - margin-bottom: 0; 329 } 330 331 /* Format tables */ 332 table { 333 - border-collapse: collapse; 334 - margin: 1.5rem 0; 335 - display: block; 336 - overflow-x: auto; 337 - white-space: nowrap; 338 } 339 340 td, 341 th { 342 - border: 1px solid var(--border); 343 - text-align: start; 344 - padding: 0.5rem; 345 } 346 347 th { 348 - background-color: var(--bg-light); 349 - font-weight: bold; 350 } 351 352 tr:nth-child(even) { 353 - background-color: var(--bg-light); 354 } 355 356 table caption { 357 - text-align: left; 358 - font-weight: bold; 359 - margin: 0 0 0.4rem 1rem; 360 } 361 362 /* format forms */ 363 fieldset { 364 - border: 1px dashed var(--accent); 365 - border-radius: var(--standard-border-radius); 366 } 367 368 - fieldset > legend { 369 - color: var(--accent); 370 } 371 372 textarea, ··· 374 input, 375 button, 376 .button { 377 - font-size: inherit; 378 - font-family: inherit; 379 - padding: 0.25rem; 380 - border-radius: var(--standard-border-radius); 381 - box-shadow: none; 382 - max-width: 100%; 383 - display: inline-block; 384 } 385 386 textarea, 387 select, 388 input { 389 - color: var(--text); 390 - background-color: var(--bg); 391 - border: 1px dashed var(--border); 392 } 393 394 label { 395 - display: block; 396 } 397 398 fieldset label { 399 - margin: 0 0 0.3rem 0; 400 } 401 402 textarea { 403 - max-width: 43.5rem; 404 - resize: both; 405 } 406 407 textarea:not([cols]) { 408 - width: 100%; 409 } 410 411 @media only screen and (max-width: 720px) { 412 - textarea, 413 - select, 414 - input { 415 - width: 100%; 416 - } 417 } 418 419 /* format buttons */ ··· 424 input[type="reset"], 425 input[type="button"], 426 label[type="button"] { 427 - border: 1px solid var(--accent); 428 - background-color: var(--accent); 429 - color: var(--accent-text); 430 - padding: 0.5rem 0.9rem; 431 - text-decoration: none; 432 - line-height: normal; 433 } 434 435 .button[aria-disabled="true"], ··· 437 textarea:disabled, 438 select:disabled, 439 button[disabled] { 440 - cursor: not-allowed; 441 - background-color: var(--bg-light); 442 - border-color: var(--bg-light); 443 - color: var(--text-light); 444 } 445 446 input[type="range"] { 447 - padding: 0; 448 - color: var(--accent); 449 } 450 451 abbr[title] { 452 - cursor: help; 453 - text-decoration-line: underline; 454 - text-decoration-style: dotted; 455 } 456 457 button:enabled:hover, ··· 460 input[type="reset"]:enabled:hover, 461 input[type="button"]:enabled:hover, 462 label[type="button"]:hover { 463 - background-color: var(--accent-light); 464 - border-color: var(--accent-light); 465 - cursor: pointer; 466 } 467 468 .button:focus-visible, 469 button:focus-visible:where(:enabled), 470 - input:enabled:focus-visible:where( 471 - [type="submit"], 472 - [type="reset"], 473 - [type="button"] 474 - ) { 475 - outline: 2px solid var(--accent); 476 - outline-offset: 1px; 477 } 478 479 /* checkbox and radio button style */ 480 input[type="checkbox"], 481 input[type="radio"] { 482 - vertical-align: middle; 483 - position: relative; 484 - width: min-content; 485 - width: 14px; 486 - height: 14px; 487 } 488 489 - input[type="checkbox"] + label, 490 - input[type="radio"] + label { 491 - display: inline-block; 492 } 493 494 input[type="radio"] { 495 - border-radius: 100%; 496 } 497 498 input[type="checkbox"]:checked, 499 input[type="radio"]:checked { 500 - background-color: var(--accent); 501 } 502 503 @media only screen and (max-width: 720px) { 504 - textarea, 505 - select, 506 - input { 507 - width: 100%; 508 - } 509 } 510 511 input[type="color"] { 512 - height: 2.5rem; 513 - padding: 0.2rem; 514 } 515 516 input[type="file"] { 517 - border: 0; 518 } 519 520 /* misc body elements */ 521 hr { 522 - border: 1px dashed var(--accent); 523 - margin: 0.5rem 0 0.5rem 0; 524 } 525 526 mark { 527 - padding: 0 0.25em 0 0.25em; 528 - border-radius: var(--standard-border-radius); 529 - background-color: var(--accent); 530 - color: var(--bg); 531 } 532 533 mark a { 534 - color: var(--link); 535 } 536 537 img, 538 video, 539 iframe[src^="https://www.youtube-nocookie.com"], 540 iframe[src^="https://www.youtube.com"] { 541 - max-width: 90%; 542 - height: auto; 543 - padding: 0.125rem; 544 - border: dashed 2px var(--accent); 545 - border-radius: 15px; 546 } 547 548 figure { 549 - margin: 0; 550 - display: block; 551 - overflow-x: auto; 552 } 553 554 figcaption { 555 - text-align: left; 556 - font-size: 0.875rem; 557 - color: var(--text-light); 558 - margin: 0 0 1rem 1rem; 559 } 560 561 blockquote { 562 - margin: 0 0 0 1.25rem; 563 - padding: 0.5rem 0 0 0.5rem; 564 - border-inline-start: 0.375rem solid var(--accent); 565 - color: var(--text-light); 566 - font-style: italic; 567 } 568 569 p:has(cite) { 570 - text-align: right; 571 - font-size: 0.875rem; 572 - color: var(--text-light); 573 - font-weight: 600; 574 } 575 576 cite::before { 577 - content: "โ€” "; 578 } 579 580 dt { 581 - color: var(--text-light); 582 } 583 584 code, ··· 586 pre span, 587 kbd, 588 samp { 589 - font-family: var(--mono-font); 590 } 591 592 pre { 593 - border: 1px solid var(--accent); 594 - max-height: 30rem; 595 - padding: 0.625rem; 596 - overflow-x: auto; 597 - font-style: monospace; 598 } 599 600 p code, 601 li code, 602 div code { 603 - padding: 0 0.125rem 0 0.125rem; 604 - border-radius: 3px; 605 - color: var(--bg); 606 - background-color: var(--text); 607 - transition: background-color 0.3s ease; 608 } 609 610 pre code { 611 - padding: 0; 612 - border-radius: 0; 613 - color: inherit; 614 - background-color: inherit; 615 } 616 617 iframe { 618 - max-width: 90%; 619 } 620 621 /* progress bars */ 622 progress { 623 - width: 100%; 624 } 625 626 progress:indeterminate { 627 - background-color: var(--bg-light); 628 } 629 630 progress::-webkit-progress-bar { 631 - border-radius: var(--standard-border-radius); 632 - background-color: var(--bg-light); 633 } 634 635 progress::-webkit-progress-value { 636 - border-radius: var(--standard-border-radius); 637 - background-color: var(--accent); 638 } 639 640 progress::-moz-progress-bar { 641 - border-radius: var(--standard-border-radius); 642 - background-color: var(--accent); 643 - transition-property: width; 644 - transition-duration: 0.3s; 645 } 646 647 progress:indeterminate::-moz-progress-bar { 648 - background-color: var(--bg-light); 649 } 650 651 dialog { 652 - max-width: 40rem; 653 - margin: auto; 654 } 655 656 dialog::backdrop { 657 - background-color: var(--bg); 658 - opacity: 0.8; 659 } 660 661 @media only screen and (max-width: 720px) { 662 - dialog { 663 - max-width: 100%; 664 - margin: auto 1em; 665 - } 666 } 667 668 /* superscript & subscript */ 669 /* prevent scripts from affecting line-height. */ 670 sup, 671 sub { 672 - vertical-align: baseline; 673 - position: relative; 674 } 675 676 sup { 677 - top: -0.4em; 678 } 679 680 sub { 681 - top: 0.3em; 682 - }
··· 1 :root, 2 ::backdrop { 3 + /* set sans-serif & mono fonts */ 4 + --sans-font: 5 + -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, 6 + "Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica, 7 + "Helvetica Neue", sans-serif; 8 + --serif-font: 9 + Superclarendon, "Bookman Old Style", "URW Bookman", "URW Bookman L", 10 + "Georgia Pro", Georgia, serif; 11 + --mono-font: 12 + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, 13 + "DejaVu Sans Mono", monospace; 14 + --standard-border-radius: 5px; 15 16 + /* default colors */ 17 + color-scheme: dark; 18 + --bg: var(--purple-night); 19 + --noise-strength: 0.15; 20 + --bg-light: var(--ultra-violet); 21 + --text: var(--lavendar-breeze); 22 + --text-light: var(--pink-puree); 23 + --text-dark: oklch(80.28% 0.0111 204.11); 24 + --accent: var(--rose-quartz); 25 + --accent-dark: var(--dark-crushed-grape); 26 + --accent-text: var(--purple-gray); 27 + --link: var(--light-crushed-grape); 28 + --link-visited: var(--red-crushed-grape); 29 + --border: var(--pink-puree); 30 + --selection: color-mix(in oklab, var(--accent), var(--purple-night) 50%); 31 } 32 33 ::selection, 34 ::-moz-selection { 35 + color: var(--bg); 36 + background: var(--selection); 37 } 38 39 /* chromium scrollbars */ 40 ::-webkit-scrollbar { 41 + width: 8px; 42 + height: 8px; 43 + overflow: visible; 44 } 45 + 46 ::-webkit-scrollbar-thumb { 47 + background: var(--accent); 48 + width: 12px; 49 } 50 + 51 ::-webkit-scrollbar-track { 52 + background: var(--bg-light); 53 } 54 55 /* firefox scrollbars */ 56 * { 57 + scrollbar-color: var(--accent) var(--bg-light); 58 + scrollbar-width: auto; 59 } 60 61 html { 62 + color-scheme: light dark; 63 + font-family: var(--mono-font); 64 + scroll-behavior: smooth; 65 } 66 67 body { 68 + min-height: 100svh; 69 + color: var(--text); 70 + background: var(--bg); 71 + position: relative; 72 + font-size: 1rem; 73 + display: grid; 74 + grid-template-columns: 1fr min(45rem, 90%) 1fr; 75 + grid-template-rows: auto 1fr auto; 76 + grid-row-gap: 0.625rem; 77 } 78 + 79 + body>* { 80 + grid-column: 2; 81 } 82 83 + body>footer { 84 + color: var(--text-light); 85 + font-size: 0.875; 86 } 87 88 /* Format headers */ 89 h1 { 90 + font-size: 2rem; 91 } 92 + 93 h2 { 94 + font-size: 1.75rem; 95 } 96 + 97 h3 { 98 + font-size: 1.5rem; 99 } 100 + 101 h4 { 102 + font-size: 1.25rem; 103 } 104 + 105 h5 { 106 + font-size: 1rem; 107 } 108 + 109 h6 { 110 + font-size: 0.75rem; 111 } 112 113 h1, ··· 116 h4, 117 h5, 118 h6 { 119 + margin: 0.5em 0 0.5em 0; 120 + padding: 0.22em 0.4em 0.22em 0.4em; 121 + background-color: var(--accent); 122 + border-bottom: 5px solid var(--bg-light); 123 + border-radius: 0.2em 0.2em 0.27em 0.27em; 124 + color: var(--accent-text); 125 + width: fit-content; 126 } 127 128 /* Fix line height when title wraps */ 129 h1, 130 h2, 131 h3 { 132 + line-height: 1.1; 133 } 134 135 @media only screen and (max-width: 720px) { 136 + h1 { 137 + font-size: 1.5rem; 138 + } 139 + 140 + h2 { 141 + font-size: 1.25rem; 142 + } 143 + 144 + h3 { 145 + font-size: 1rem; 146 + } 147 + 148 + h4 { 149 + font-size: 0.75rem; 150 + } 151 + 152 + h5 { 153 + font-size: 0.5rem; 154 + } 155 + 156 + h6 { 157 + font-size: 0.25rem; 158 + } 159 } 160 161 p { 162 + margin: 1rem 0; 163 } 164 165 /* format links */ 166 + a { 167 + color: var(--link); 168 + text-decoration: none; 169 + font-weight: 600; 170 + transition: color 120ms ease; 171 + } 172 + 173 a:visited { 174 + color: var(--link-visited); 175 } 176 177 + a:hover, 178 + a:focus-visible { 179 + color: var(--accent); 180 + outline: none; 181 + } 182 + 183 + a:visited:hover, 184 + a:visited:focus-visible { 185 + color: var(--accent); 186 } 187 188 /* format lists */ 189 ul { 190 + list-style: none; 191 + margin-top: 0.25rem; 192 + margin-bottom: 0.25rem; 193 } 194 195 ol { 196 + list-style-type: decimal; 197 + margin-top: 0.25rem; 198 + margin-bottom: 0.25rem; 199 } 200 201 li { 202 + margin-bottom: 0.125rem; 203 } 204 205 ul li::marker { 206 + content: "* "; 207 + color: var(--accent); 208 + font-size: 1.1rem; 209 } 210 211 ol li::marker { 212 + color: var(--accent); 213 } 214 215 ol li:hover::marker { 216 + font-weight: 700; 217 + color: var(--link); 218 } 219 220 /* Use flexbox to allow items to wrap, as needed */ 221 + header>nav ul, 222 + header>nav ol { 223 + display: flex; 224 + flex-direction: row; 225 + flex-wrap: wrap; 226 + align-content: space-around; 227 + justify-content: right; 228 + list-style-type: none; 229 + margin: 0.5rem 0 0 0; 230 + padding: 0; 231 + gap: 1rem; 232 } 233 234 /* List items are inline elements, make them behave more like blocks */ 235 + header>nav ul li, 236 + header>nav ol li { 237 + display: inline-block; 238 } 239 240 /* Consolidate box styling */ 241 aside, 242 details, 243 progress { 244 + background-color: var(--bg-light); 245 + border-radius: var(--standard-border-radius); 246 } 247 248 aside { 249 + font-size: 1rem; 250 + width: 35%; 251 + padding: 0 10px; 252 + margin-inline-start: 10px; 253 + float: right; 254 } 255 + 256 *[dir="rtl"] aside { 257 + float: left; 258 } 259 260 /* make aside full-width on mobile */ 261 @media only screen and (max-width: 720px) { 262 + aside { 263 + width: 100%; 264 + float: none; 265 + margin-inline-start: 0; 266 + } 267 } 268 269 details { 270 + padding: 0.5rem; 271 } 272 273 summary { 274 + cursor: pointer; 275 + font-weight: bold; 276 + word-break: break-all; 277 } 278 279 + details[open]>summary+* { 280 + margin-top: 0; 281 } 282 283 + details[open]>summary { 284 + margin-bottom: 0.5rem; 285 } 286 287 + details[open]> :last-child { 288 + margin-bottom: 0; 289 } 290 291 /* Format tables */ 292 table { 293 + border-collapse: collapse; 294 + margin: 1.5rem 0; 295 + display: block; 296 + overflow-x: auto; 297 + white-space: nowrap; 298 } 299 300 td, 301 th { 302 + border: 1px solid var(--border); 303 + text-align: start; 304 + padding: 0.5rem; 305 } 306 307 th { 308 + background-color: var(--bg-light); 309 + font-weight: bold; 310 } 311 312 tr:nth-child(even) { 313 + background-color: var(--bg-light); 314 } 315 316 table caption { 317 + text-align: left; 318 + font-weight: bold; 319 + margin: 0 0 0.4rem 1rem; 320 } 321 322 /* format forms */ 323 fieldset { 324 + border: 1px dashed var(--accent); 325 + border-radius: var(--standard-border-radius); 326 } 327 328 + fieldset>legend { 329 + color: var(--accent); 330 } 331 332 textarea, ··· 334 input, 335 button, 336 .button { 337 + font-size: inherit; 338 + font-family: inherit; 339 + padding: 0.25rem; 340 + border-radius: var(--standard-border-radius); 341 + box-shadow: none; 342 + max-width: 100%; 343 + display: inline-block; 344 } 345 346 textarea, 347 select, 348 input { 349 + color: var(--text); 350 + background-color: var(--bg); 351 + border: 1px dashed var(--border); 352 } 353 354 label { 355 + display: block; 356 } 357 358 fieldset label { 359 + margin: 0 0 0.3rem 0; 360 } 361 362 textarea { 363 + max-width: 43.5rem; 364 + resize: both; 365 } 366 367 textarea:not([cols]) { 368 + width: 100%; 369 } 370 371 @media only screen and (max-width: 720px) { 372 + 373 + textarea, 374 + select, 375 + input { 376 + width: 100%; 377 + } 378 } 379 380 /* format buttons */ ··· 385 input[type="reset"], 386 input[type="button"], 387 label[type="button"] { 388 + border: 1px solid var(--accent); 389 + background-color: var(--accent); 390 + color: var(--accent-text); 391 + padding: 0.5rem 0.9rem; 392 + text-decoration: none; 393 + line-height: normal; 394 } 395 396 .button[aria-disabled="true"], ··· 398 textarea:disabled, 399 select:disabled, 400 button[disabled] { 401 + cursor: not-allowed; 402 + background-color: var(--bg-light); 403 + border-color: var(--bg-light); 404 + color: var(--text-light); 405 } 406 407 input[type="range"] { 408 + padding: 0; 409 + color: var(--accent); 410 } 411 412 abbr[title] { 413 + cursor: help; 414 + text-decoration-line: underline; 415 + text-decoration-style: dotted; 416 } 417 418 button:enabled:hover, ··· 421 input[type="reset"]:enabled:hover, 422 input[type="button"]:enabled:hover, 423 label[type="button"]:hover { 424 + background-color: var(--accent-dark); 425 + border-color: var(--accent-dark); 426 + cursor: pointer; 427 } 428 429 .button:focus-visible, 430 button:focus-visible:where(:enabled), 431 + input:enabled:focus-visible:where([type="submit"], 432 + [type="reset"], 433 + [type="button"]) { 434 + outline: 2px solid var(--accent); 435 + outline-offset: 1px; 436 } 437 438 /* checkbox and radio button style */ 439 input[type="checkbox"], 440 input[type="radio"] { 441 + vertical-align: middle; 442 + position: relative; 443 + width: 14px; 444 + height: 14px; 445 } 446 447 + input[type="checkbox"]+label, 448 + input[type="radio"]+label { 449 + display: inline-block; 450 } 451 452 input[type="radio"] { 453 + border-radius: 100%; 454 } 455 456 input[type="checkbox"]:checked, 457 input[type="radio"]:checked { 458 + background-color: var(--accent); 459 } 460 461 @media only screen and (max-width: 720px) { 462 + 463 + textarea, 464 + select, 465 + input { 466 + width: 100%; 467 + } 468 } 469 470 input[type="color"] { 471 + height: 2.5rem; 472 + padding: 0.2rem; 473 } 474 475 input[type="file"] { 476 + border: 0; 477 } 478 479 /* misc body elements */ 480 hr { 481 + border: 1px dashed var(--accent); 482 + margin: 0.5rem 0 0.5rem 0; 483 } 484 485 mark { 486 + padding: 0 0.25em 0 0.25em; 487 + border-radius: var(--standard-border-radius); 488 + background-color: var(--accent); 489 + color: var(--bg); 490 } 491 492 mark a { 493 + color: var(--link); 494 } 495 496 img, 497 video, 498 iframe[src^="https://www.youtube-nocookie.com"], 499 iframe[src^="https://www.youtube.com"] { 500 + max-width: 100%; 501 + height: auto; 502 + border-radius: 0.35rem; 503 + } 504 + 505 + .img-container { 506 + background-color: var(--accent); 507 + border-bottom: 4px solid var(--bg-light); 508 + border-radius: 7px 7px 10px 10px; 509 + padding: 0.35rem; 510 + margin: 1rem; 511 + display: inline-block; 512 + max-width: 90%; 513 } 514 515 figure { 516 + margin: 0; 517 + display: block; 518 + overflow-x: auto; 519 } 520 521 figcaption { 522 + text-align: left; 523 + font-size: 0.875rem; 524 + color: var(--text-light); 525 + margin: 0 0 1rem 1rem; 526 } 527 528 blockquote { 529 + margin: 0 0 0 1.25rem; 530 + padding: 0.5rem 0 0 0.5rem; 531 + border-inline-start: 0.375rem solid var(--accent); 532 + color: var(--text-light); 533 + font-style: italic; 534 + } 535 + 536 + /* Callout styles */ 537 + .callout { 538 + margin: 1.5rem 0; 539 + padding: 1rem; 540 + border-left: 0.25rem solid; 541 + border-radius: 0.25rem; 542 + background-color: var(--bg-light); 543 + } 544 + 545 + .callout-title { 546 + display: flex; 547 + align-items: center; 548 + gap: 0.5rem; 549 + margin-bottom: 0.5rem; 550 + font-size: 1rem; 551 + } 552 + 553 + .callout-icon { 554 + display: inline-flex; 555 + width: 1.25rem; 556 + height: 1.25rem; 557 + flex-shrink: 0; 558 + } 559 + 560 + .callout-icon svg { 561 + width: 100%; 562 + height: 100%; 563 + } 564 + 565 + .callout-content { 566 + font-style: normal; 567 + color: var(--text); 568 + } 569 + 570 + .callout-content p:first-child { 571 + margin-top: 0; 572 + } 573 + 574 + .callout-content p:last-child { 575 + margin-bottom: 0; 576 + } 577 + 578 + .callout-blue { 579 + border-color: #8aadf4; 580 + } 581 + 582 + .callout-blue .callout-icon { 583 + color: #8aadf4; 584 + } 585 + 586 + .callout-yellow { 587 + border-color: #eed49f; 588 + } 589 + 590 + .callout-yellow .callout-icon { 591 + color: #eed49f; 592 + } 593 + 594 + .callout-red { 595 + border-color: #ed8796; 596 } 597 598 + .callout-red .callout-icon { 599 + color: #ed8796; 600 + } 601 + 602 + .callout-green { 603 + border-color: #a6da95; 604 + } 605 + 606 + .callout-green .callout-icon { 607 + color: #a6da95; 608 + } 609 + 610 + .callout-gray { 611 + border-color: #6e738d; 612 + } 613 + 614 + .callout-gray .callout-icon { 615 + color: #6e738d; 616 + } 617 + 618 + 619 p:has(cite) { 620 + text-align: right; 621 + font-size: 0.875rem; 622 + color: var(--text-light); 623 + font-weight: 600; 624 } 625 626 cite::before { 627 + content: "โ€” "; 628 } 629 630 dt { 631 + color: var(--text-light); 632 } 633 634 code, ··· 636 pre span, 637 kbd, 638 samp { 639 + font-family: var(--mono-font); 640 } 641 642 pre { 643 + border: 1px solid var(--accent); 644 + max-height: 30rem; 645 + padding: 0.625rem; 646 + overflow-x: auto; 647 + font-style: monospace; 648 + } 649 + 650 + /* Allow wrapping for specific code blocks (e.g., SSH keys) */ 651 + pre[data-lang="pub"], 652 + pre.wrap { 653 + white-space: pre-wrap; 654 + word-break: break-all; 655 + overflow-x: visible; 656 } 657 658 p code, 659 li code, 660 div code { 661 + padding: 0 0.125rem 0 0.125rem; 662 + border-radius: 3px; 663 + color: var(--bg); 664 + background-color: var(--text); 665 } 666 667 pre code { 668 + padding: 0; 669 + border-radius: 0; 670 + color: inherit; 671 + background-color: inherit; 672 } 673 674 iframe { 675 + max-width: 90%; 676 } 677 678 /* progress bars */ 679 progress { 680 + width: 100%; 681 } 682 683 progress:indeterminate { 684 + background-color: var(--bg-light); 685 } 686 687 progress::-webkit-progress-bar { 688 + border-radius: var(--standard-border-radius); 689 + background-color: var(--bg-light); 690 } 691 692 progress::-webkit-progress-value { 693 + border-radius: var(--standard-border-radius); 694 + background-color: var(--accent); 695 } 696 697 progress::-moz-progress-bar { 698 + border-radius: var(--standard-border-radius); 699 + background-color: var(--accent); 700 + transition-property: width; 701 + transition-duration: 0.3s; 702 } 703 704 progress:indeterminate::-moz-progress-bar { 705 + background-color: var(--bg-light); 706 } 707 708 dialog { 709 + max-width: 40rem; 710 + margin: auto; 711 } 712 713 dialog::backdrop { 714 + background-color: var(--bg); 715 + opacity: 0.8; 716 } 717 718 @media only screen and (max-width: 720px) { 719 + dialog { 720 + max-width: 100%; 721 + margin: auto 1em; 722 + } 723 } 724 725 /* superscript & subscript */ 726 /* prevent scripts from affecting line-height. */ 727 sup, 728 sub { 729 + vertical-align: baseline; 730 + position: relative; 731 } 732 733 sup { 734 + top: -0.4em; 735 } 736 737 sub { 738 + top: 0.3em; 739 + }
+207 -492
sass/css/syntax-theme.css
··· 2 * theme "Catppuccin" generated by syntect 3 */ 4 5 - @supports not (-moz-appearance: none) { 6 - .z-code { 7 - transition: background-color 0.3s ease; 8 - } 9 - } 10 11 - html[data-theme="light"] .z-code { 12 - color: #4c4f69; 13 - background-color: #f2efea; 14 } 15 16 - html[data-theme="light"] .z-comment { 17 - color: #9ca0b0; 18 - font-style: italic; 19 } 20 - html[data-theme="light"] .z-string { 21 - color: #40a02b; 22 } 23 - html[data-theme="light"] .z-string.z-regexp { 24 - color: #fe640b; 25 } 26 - html[data-theme="light"] .z-constant.z-numeric { 27 - color: #fe640b; 28 } 29 - html[data-theme="light"] .z-constant.z-language.z-boolean { 30 - color: #fe640b; 31 - font-weight: bold; 32 - font-style: italic; 33 } 34 - html[data-theme="light"] .z-constant.z-language { 35 - color: #7287fd; 36 - font-style: italic; 37 } 38 - html[data-theme="light"] .z-support.z-function.z-builtin { 39 - color: #fe640b; 40 - font-style: italic; 41 } 42 - html[data-theme="light"] .z-variable.z-other.z-constant { 43 - color: #fe640b; 44 } 45 - html[data-theme="light"] .z-keyword { 46 - color: #d20f39; 47 - font-style: italic; 48 } 49 - html[data-theme="light"] .z-keyword.z-control.z-loop, 50 - html[data-theme="light"] .z-keyword.z-control.z-conditional, 51 - html[data-theme="light"] .z-keyword.z-control.z-c\+\+ { 52 - color: #8839ef; 53 - font-weight: bold; 54 } 55 - html[data-theme="light"] .z-keyword.z-control.z-return, 56 - html[data-theme="light"] .z-keyword.z-control.z-flow.z-return { 57 - color: #ea76cb; 58 - font-weight: bold; 59 } 60 - html[data-theme="light"] .z-support.z-type.z-exception { 61 - color: #fe640b; 62 - font-style: italic; 63 } 64 - html[data-theme="light"] .z-keyword.z-operator, 65 - html[data-theme="light"] .z-punctuation.z-accessor { 66 - color: #04a5e5; 67 - font-weight: bold; 68 } 69 - html[data-theme="light"] .z-punctuation.z-separator { 70 - color: #179299; 71 } 72 - html[data-theme="light"] .z-punctuation.z-terminator { 73 - color: #179299; 74 } 75 - html[data-theme="light"] .z-punctuation.z-section { 76 - color: #7c7f93; 77 } 78 - html[data-theme="light"] .z-keyword.z-control.z-import.z-include { 79 - color: #179299; 80 - font-style: italic; 81 } 82 - html[data-theme="light"] .z-storage { 83 - color: #d20f39; 84 } 85 - html[data-theme="light"] .z-storage.z-type { 86 - color: #df8e1d; 87 - font-style: italic; 88 } 89 - html[data-theme="light"] .z-storage.z-modifier { 90 - color: #d20f39; 91 } 92 - html[data-theme="light"] .z-entity.z-name.z-namespace, 93 - html[data-theme="light"] .z-meta.z-path { 94 - color: #dc8a78; 95 - font-style: italic; 96 } 97 - html[data-theme="light"] .z-storage.z-type.z-class { 98 - color: #dc8a78; 99 - font-style: italic; 100 } 101 - html[data-theme="light"] .z-entity.z-name.z-label { 102 - color: #1e66f5; 103 } 104 - html[data-theme="light"] .z-keyword.z-declaration.z-class { 105 - color: #d20f39; 106 - font-style: italic; 107 } 108 - html[data-theme="light"] .z-entity.z-name.z-class, 109 - html[data-theme="light"] .z-meta.z-toc-list.z-full-identifier { 110 - color: #04a5e5; 111 } 112 - html[data-theme="light"] .z-entity.z-other.z-inherited-class { 113 - color: #04a5e5; 114 - font-style: italic; 115 - } 116 - html[data-theme="light"] .z-entity.z-name.z-function, 117 - html[data-theme="light"] .z-variable.z-function { 118 - color: #1e66f5; 119 - font-style: italic; 120 - } 121 - html[data-theme="light"] .z-entity.z-name.z-function.z-preprocessor { 122 - color: #d20f39; 123 - } 124 - html[data-theme="light"] .z-keyword.z-control.z-import { 125 - color: #d20f39; 126 - } 127 - html[data-theme="light"] .z-entity.z-name.z-function.z-constructor, 128 - html[data-theme="light"] .z-entity.z-name.z-function.z-destructor { 129 - color: #7287fd; 130 - } 131 - html[data-theme="light"] .z-variable.z-parameter.z-function { 132 - color: #dc8a78; 133 - font-style: italic; 134 } 135 - html[data-theme="light"] .z-keyword.z-declaration.z-function { 136 - color: #e64553; 137 - font-style: italic; 138 } 139 - html[data-theme="light"] .z-support.z-function { 140 - color: #04a5e5; 141 } 142 - html[data-theme="light"] .z-support.z-constant { 143 - color: #1e66f5; 144 } 145 - html[data-theme="light"] .z-support.z-type, 146 - html[data-theme="light"] .z-support.z-class { 147 - color: #1e66f5; 148 - font-style: italic; 149 } 150 - html[data-theme="light"] .z-variable.z-function { 151 - color: #1e66f5; 152 - font-style: italic; 153 } 154 - html[data-theme="light"] .z-variable.z-parameter { 155 - color: #dc8a78; 156 - font-style: italic; 157 } 158 - html[data-theme="light"] .z-variable.z-other { 159 - color: #4c4f69; 160 - font-style: italic; 161 } 162 - html[data-theme="light"] .z-variable.z-other.z-member { 163 - color: #dc8a78; 164 } 165 - html[data-theme="light"] .z-variable.z-language { 166 - color: #179299; 167 } 168 - html[data-theme="light"] .z-entity.z-name.z-tag { 169 - color: #fe640b; 170 } 171 - html[data-theme="light"] .z-entity.z-other.z-attribute-name { 172 - color: #8839ef; 173 - font-style: italic; 174 } 175 - html[data-theme="light"] .z-punctuation.z-definition.z-tag { 176 - color: #e64553; 177 } 178 - html[data-theme="light"] .z-markup.z-underline.z-link.z-markdown { 179 - color: #dc8a78; 180 - text-decoration: underline; 181 - font-style: italic; 182 } 183 - html[data-theme="light"] .z-meta.z-link.z-inline.z-description { 184 - color: #7287fd; 185 - font-weight: bold; 186 } 187 - html[data-theme="light"] .z-comment.z-block.z-markdown, 188 - html[data-theme="light"] .z-meta.z-code-fence, 189 - html[data-theme="light"] .z-markup.z-raw.z-code-fence, 190 - html[data-theme="light"] .z-markup.z-raw.z-inline { 191 - color: #179299; 192 - font-style: italic; 193 } 194 - html[data-theme="light"] .z-punctuation.z-definition.z-heading, 195 - html[data-theme="light"] .z-entity.z-name.z-section { 196 - color: #1e66f5; 197 - font-weight: bold; 198 } 199 - html[data-theme="light"] .z-markup.z-italic { 200 - color: #e64553; 201 - font-style: italic; 202 } 203 - html[data-theme="light"] .z-markup.z-bold { 204 - color: #e64553; 205 - font-weight: bold; 206 } 207 - html[data-theme="light"] .z-constant.z-character.z-escape { 208 - color: #ea76cb; 209 } 210 - html[data-theme="light"] 211 - .z-source.z-shell.z-bash 212 - .z-meta.z-function.z-shell 213 - .z-meta.z-compound.z-shell 214 - .z-meta.z-function-call.z-identifier.z-shell { 215 - color: #ea76cb; 216 } 217 - html[data-theme="light"] .z-variable.z-language.z-shell { 218 - color: #d20f39; 219 - font-style: italic; 220 } 221 - html[data-theme="light"] 222 - .z-source.z-lua 223 - .z-meta.z-function.z-lua 224 - .z-meta.z-block.z-lua 225 - .z-meta.z-mapping.z-value.z-lua 226 - .z-meta.z-mapping.z-key.z-lua 227 - .z-string.z-unquoted.z-key.z-lua { 228 - color: #7287fd; 229 - font-style: italic; 230 } 231 - html[data-theme="light"] 232 - .z-source.z-lua 233 - .z-meta.z-function.z-lua 234 - .z-meta.z-block.z-lua 235 - .z-meta.z-mapping.z-key.z-lua 236 - .z-string.z-unquoted.z-key.z-lua { 237 - color: #dd7878; 238 } 239 - html[data-theme="light"] .z-entity.z-name.z-constant.z-java { 240 - color: #179299; 241 } 242 - html[data-theme="light"] .z-support.z-type.z-property-name.z-css { 243 - color: #dd7878; 244 - font-style: italic; 245 } 246 - html[data-theme="light"] .z-support.z-constant.z-property-value.z-css { 247 - color: #4c4f69; 248 - } 249 - html[data-theme="light"] .z-constant.z-numeric.z-suffix.z-css, 250 - html[data-theme="light"] .z-keyword.z-other.z-unit.z-css { 251 - color: #179299; 252 - font-style: italic; 253 - } 254 - html[data-theme="light"] .z-variable.z-other.z-custom-property.z-name.z-css, 255 - html[data-theme="light"] .z-support.z-type.z-custom-property.z-name.z-css, 256 - html[data-theme="light"] .z-punctuation.z-definition.z-custom-property.z-css { 257 - color: #179299; 258 - } 259 - html[data-theme="light"] .z-entity.z-name.z-tag.z-css { 260 - color: #7287fd; 261 - } 262 - html[data-theme="light"] .z-variable.z-other.z-sass { 263 - color: #179299; 264 - } 265 - html[data-theme="light"] .z-invalid { 266 - color: #4c4f69; 267 - background-color: #d20f39; 268 - } 269 - html[data-theme="light"] .z-invalid.z-deprecated { 270 - color: #4c4f69; 271 - background-color: #8839ef; 272 - } 273 - html[data-theme="light"] .z-meta.z-diff, 274 - html[data-theme="light"] .z-meta.z-diff.z-header { 275 - color: #9ca0b0; 276 - } 277 - html[data-theme="light"] .z-markup.z-deleted { 278 - color: #d20f39; 279 - } 280 - html[data-theme="light"] .z-markup.z-inserted { 281 - color: #40a02b; 282 - } 283 - html[data-theme="light"] .z-markup.z-changed { 284 - color: #df8e1d; 285 - } 286 - html[data-theme="light"] .z-message.z-error { 287 - color: #d20f39; 288 } 289 290 - /* dark */ 291 - 292 - html[data-theme="dark"] .z-code { 293 - color: #cad3f5; 294 - background-color: #2a2e35; 295 } 296 297 - html[data-theme="dark"] .z-comment { 298 - color: #6e738d; 299 - font-style: italic; 300 } 301 - html[data-theme="dark"] .z-string { 302 - color: #a6da95; 303 } 304 - html[data-theme="dark"] .z-string.z-regexp { 305 - color: #f5a97f; 306 } 307 - html[data-theme="dark"] .z-constant.z-numeric { 308 - color: #f5a97f; 309 } 310 - html[data-theme="dark"] .z-constant.z-language.z-boolean { 311 - color: #f5a97f; 312 - font-weight: bold; 313 - font-style: italic; 314 } 315 - html[data-theme="dark"] .z-constant.z-language { 316 - color: #b7bdf8; 317 - font-style: italic; 318 } 319 - html[data-theme="dark"] .z-support.z-function.z-builtin { 320 - color: #f5a97f; 321 - font-style: italic; 322 } 323 - html[data-theme="dark"] .z-variable.z-other.z-constant { 324 - color: #f5a97f; 325 } 326 - html[data-theme="dark"] .z-keyword { 327 - color: #ed8796; 328 - font-style: italic; 329 } 330 - html[data-theme="dark"] .z-keyword.z-control.z-loop, 331 - html[data-theme="dark"] .z-keyword.z-control.z-conditional, 332 - html[data-theme="dark"] .z-keyword.z-control.z-c\+\+ { 333 - color: #c6a0f6; 334 - font-weight: bold; 335 } 336 - html[data-theme="dark"] .z-keyword.z-control.z-return, 337 - html[data-theme="dark"] .z-keyword.z-control.z-flow.z-return { 338 - color: #f5bde6; 339 - font-weight: bold; 340 } 341 - html[data-theme="dark"] .z-support.z-type.z-exception { 342 - color: #f5a97f; 343 - font-style: italic; 344 } 345 - html[data-theme="dark"] .z-keyword.z-operator, 346 - html[data-theme="dark"] .z-punctuation.z-accessor { 347 - color: #91d7e3; 348 - font-weight: bold; 349 } 350 - html[data-theme="dark"] .z-punctuation.z-separator { 351 - color: #8bd5ca; 352 } 353 - html[data-theme="dark"] .z-punctuation.z-terminator { 354 - color: #8bd5ca; 355 - } 356 - html[data-theme="dark"] .z-punctuation.z-section { 357 - color: #939ab7; 358 - } 359 - html[data-theme="dark"] .z-keyword.z-control.z-import.z-include { 360 - color: #8bd5ca; 361 - font-style: italic; 362 - } 363 - html[data-theme="dark"] .z-storage { 364 - color: #ed8796; 365 - } 366 - html[data-theme="dark"] .z-storage.z-type { 367 - color: #eed49f; 368 - font-style: italic; 369 - } 370 - html[data-theme="dark"] .z-storage.z-modifier { 371 - color: #ed8796; 372 - } 373 - html[data-theme="dark"] .z-entity.z-name.z-namespace, 374 - html[data-theme="dark"] .z-meta.z-path { 375 - color: #f4dbd6; 376 - font-style: italic; 377 - } 378 - html[data-theme="dark"] .z-storage.z-type.z-class { 379 - color: #f4dbd6; 380 - font-style: italic; 381 - } 382 - html[data-theme="dark"] .z-entity.z-name.z-label { 383 - color: #8aadf4; 384 - } 385 - html[data-theme="dark"] .z-keyword.z-declaration.z-class { 386 - color: #ed8796; 387 - font-style: italic; 388 - } 389 - html[data-theme="dark"] .z-entity.z-name.z-class, 390 - html[data-theme="dark"] .z-meta.z-toc-list.z-full-identifier { 391 - color: #91d7e3; 392 - } 393 - html[data-theme="dark"] .z-entity.z-other.z-inherited-class { 394 - color: #91d7e3; 395 - font-style: italic; 396 - } 397 - html[data-theme="dark"] .z-entity.z-name.z-function, 398 - html[data-theme="dark"] .z-variable.z-function { 399 - color: #8aadf4; 400 - font-style: italic; 401 - } 402 - html[data-theme="dark"] .z-entity.z-name.z-function.z-preprocessor { 403 - color: #ed8796; 404 - } 405 - html[data-theme="dark"] .z-keyword.z-control.z-import { 406 - color: #ed8796; 407 - } 408 - html[data-theme="dark"] .z-entity.z-name.z-function.z-constructor, 409 - html[data-theme="dark"] .z-entity.z-name.z-function.z-destructor { 410 - color: #b7bdf8; 411 - } 412 - html[data-theme="dark"] .z-variable.z-parameter.z-function { 413 - color: #f4dbd6; 414 - font-style: italic; 415 - } 416 - html[data-theme="dark"] .z-keyword.z-declaration.z-function { 417 - color: #ee99a0; 418 - font-style: italic; 419 - } 420 - html[data-theme="dark"] .z-support.z-function { 421 - color: #91d7e3; 422 - } 423 - html[data-theme="dark"] .z-support.z-constant { 424 - color: #8aadf4; 425 - } 426 - html[data-theme="dark"] .z-support.z-type, 427 - html[data-theme="dark"] .z-support.z-class { 428 - color: #8aadf4; 429 - font-style: italic; 430 - } 431 - html[data-theme="dark"] .z-variable.z-function { 432 - color: #8aadf4; 433 - font-style: italic; 434 - } 435 - html[data-theme="dark"] .z-variable.z-parameter { 436 - color: #f4dbd6; 437 - font-style: italic; 438 - } 439 - html[data-theme="dark"] .z-variable.z-other { 440 - color: #cad3f5; 441 - font-style: italic; 442 - } 443 - html[data-theme="dark"] .z-variable.z-other.z-member { 444 - color: #f4dbd6; 445 - } 446 - html[data-theme="dark"] .z-variable.z-language { 447 - color: #8bd5ca; 448 - } 449 - html[data-theme="dark"] .z-entity.z-name.z-tag { 450 - color: #f5a97f; 451 - } 452 - html[data-theme="dark"] .z-entity.z-other.z-attribute-name { 453 - color: #c6a0f6; 454 - font-style: italic; 455 - } 456 - html[data-theme="dark"] .z-punctuation.z-definition.z-tag { 457 - color: #ee99a0; 458 - } 459 - html[data-theme="dark"] .z-markup.z-underline.z-link.z-markdown { 460 - color: #f4dbd6; 461 - text-decoration: underline; 462 - font-style: italic; 463 - } 464 - html[data-theme="dark"] .z-meta.z-link.z-inline.z-description { 465 - color: #b7bdf8; 466 - font-weight: bold; 467 - } 468 - html[data-theme="dark"] .z-comment.z-block.z-markdown, 469 - html[data-theme="dark"] .z-meta.z-code-fence, 470 - html[data-theme="dark"] .z-markup.z-raw.z-code-fence, 471 - html[data-theme="dark"] .z-markup.z-raw.z-inline { 472 - color: #8bd5ca; 473 - font-style: italic; 474 - } 475 - html[data-theme="dark"] .z-punctuation.z-definition.z-heading, 476 - html[data-theme="dark"] .z-entity.z-name.z-section { 477 - color: #8aadf4; 478 - font-weight: bold; 479 - } 480 - html[data-theme="dark"] .z-markup.z-italic { 481 - color: #ee99a0; 482 - font-style: italic; 483 - } 484 - html[data-theme="dark"] .z-markup.z-bold { 485 - color: #ee99a0; 486 - font-weight: bold; 487 - } 488 - html[data-theme="dark"] .z-constant.z-character.z-escape { 489 - color: #f5bde6; 490 - } 491 - html[data-theme="dark"] 492 - .z-source.z-shell.z-bash 493 - .z-meta.z-function.z-shell 494 - .z-meta.z-compound.z-shell 495 - .z-meta.z-function-call.z-identifier.z-shell { 496 - color: #f5bde6; 497 - } 498 - html[data-theme="dark"] .z-variable.z-language.z-shell { 499 - color: #ed8796; 500 - font-style: italic; 501 - } 502 - html[data-theme="dark"] 503 - .z-source.z-lua 504 - .z-meta.z-function.z-lua 505 - .z-meta.z-block.z-lua 506 - .z-meta.z-mapping.z-value.z-lua 507 - .z-meta.z-mapping.z-key.z-lua 508 - .z-string.z-unquoted.z-key.z-lua { 509 - color: #b7bdf8; 510 - font-style: italic; 511 - } 512 - html[data-theme="dark"] 513 - .z-source.z-lua 514 - .z-meta.z-function.z-lua 515 - .z-meta.z-block.z-lua 516 - .z-meta.z-mapping.z-key.z-lua 517 - .z-string.z-unquoted.z-key.z-lua { 518 - color: #f0c6c6; 519 - } 520 - html[data-theme="dark"] .z-entity.z-name.z-constant.z-java { 521 - color: #8bd5ca; 522 - } 523 - html[data-theme="dark"] .z-support.z-type.z-property-name.z-css { 524 - color: #f0c6c6; 525 - font-style: italic; 526 - } 527 - html[data-theme="dark"] .z-support.z-constant.z-property-value.z-css { 528 - color: #cad3f5; 529 - } 530 - html[data-theme="dark"] .z-constant.z-numeric.z-suffix.z-css, 531 - html[data-theme="dark"] .z-keyword.z-other.z-unit.z-css { 532 - color: #8bd5ca; 533 - font-style: italic; 534 - } 535 - html[data-theme="dark"] .z-variable.z-other.z-custom-property.z-name.z-css, 536 - html[data-theme="dark"] .z-support.z-type.z-custom-property.z-name.z-css, 537 - html[data-theme="dark"] .z-punctuation.z-definition.z-custom-property.z-css { 538 - color: #8bd5ca; 539 - } 540 - html[data-theme="dark"] .z-entity.z-name.z-tag.z-css { 541 - color: #b7bdf8; 542 - } 543 - html[data-theme="dark"] .z-variable.z-other.z-sass { 544 - color: #8bd5ca; 545 - } 546 - html[data-theme="dark"] .z-invalid { 547 - color: #cad3f5; 548 - background-color: #ed8796; 549 - } 550 - html[data-theme="dark"] .z-invalid.z-deprecated { 551 - color: #cad3f5; 552 - background-color: #c6a0f6; 553 - } 554 - html[data-theme="dark"] .z-meta.z-diff, 555 - html[data-theme="dark"] .z-meta.z-diff.z-header { 556 - color: #6e738d; 557 - } 558 - html[data-theme="dark"] .z-markup.z-deleted { 559 - color: #ed8796; 560 - } 561 - html[data-theme="dark"] .z-markup.z-inserted { 562 - color: #a6da95; 563 - } 564 - html[data-theme="dark"] .z-markup.z-changed { 565 - color: #eed49f; 566 - } 567 - html[data-theme="dark"] .z-message.z-error { 568 - color: #ed8796; 569 }
··· 2 * theme "Catppuccin" generated by syntect 3 */ 4 5 + /* dark */ 6 7 + .z-code { 8 + color: #cad3f5; 9 + background-color: var(--nightshade-violet); 10 } 11 12 + .z-comment { 13 + color: #6e738d; 14 + font-style: italic; 15 } 16 + .z-string { 17 + color: #a6da95; 18 } 19 + .z-string.z-regexp { 20 + color: #f5a97f; 21 } 22 + .z-constant.z-numeric { 23 + color: #f5a97f; 24 } 25 + .z-constant.z-language.z-boolean { 26 + color: #f5a97f; 27 + font-weight: bold; 28 + font-style: italic; 29 } 30 + .z-constant.z-language { 31 + color: #b7bdf8; 32 + font-style: italic; 33 } 34 + .z-support.z-function.z-builtin { 35 + color: #f5a97f; 36 + font-style: italic; 37 } 38 + .z-variable.z-other.z-constant { 39 + color: #f5a97f; 40 } 41 + .z-keyword { 42 + color: #ed8796; 43 + font-style: italic; 44 } 45 + .z-keyword.z-control.z-loop, 46 + .z-keyword.z-control.z-conditional, 47 + .z-keyword.z-control.z-c\+\+ { 48 + color: #c6a0f6; 49 + font-weight: bold; 50 } 51 + .z-keyword.z-control.z-return, 52 + .z-keyword.z-control.z-flow.z-return { 53 + color: #f5bde6; 54 + font-weight: bold; 55 } 56 + .z-support.z-type.z-exception { 57 + color: #f5a97f; 58 + font-style: italic; 59 } 60 + .z-keyword.z-operator, 61 + .z-punctuation.z-accessor { 62 + color: #91d7e3; 63 + font-weight: bold; 64 } 65 + .z-punctuation.z-separator { 66 + color: #8bd5ca; 67 } 68 + .z-punctuation.z-terminator { 69 + color: #8bd5ca; 70 } 71 + .z-punctuation.z-section { 72 + color: #939ab7; 73 } 74 + .z-keyword.z-control.z-import.z-include { 75 + color: #8bd5ca; 76 + font-style: italic; 77 } 78 + .z-storage { 79 + color: #ed8796; 80 } 81 + .z-storage.z-type { 82 + color: #eed49f; 83 + font-style: italic; 84 } 85 + .z-storage.z-modifier { 86 + color: #ed8796; 87 } 88 + .z-entity.z-name.z-namespace, 89 + .z-meta.z-path { 90 + color: #f4dbd6; 91 + font-style: italic; 92 } 93 + .z-storage.z-type.z-class { 94 + color: #f4dbd6; 95 + font-style: italic; 96 } 97 + .z-entity.z-name.z-label { 98 + color: #8aadf4; 99 } 100 + .z-keyword.z-declaration.z-class { 101 + color: #ed8796; 102 + font-style: italic; 103 } 104 + .z-entity.z-name.z-class, 105 + .z-meta.z-toc-list.z-full-identifier { 106 + color: #91d7e3; 107 } 108 + .z-entity.z-other.z-inherited-class { 109 + color: #91d7e3; 110 + font-style: italic; 111 } 112 + .z-entity.z-name.z-function, 113 + .z-variable.z-function { 114 + color: #8aadf4; 115 + font-style: italic; 116 } 117 + .z-entity.z-name.z-function.z-preprocessor { 118 + color: #ed8796; 119 } 120 + .z-keyword.z-control.z-import { 121 + color: #ed8796; 122 } 123 + .z-entity.z-name.z-function.z-constructor, 124 + .z-entity.z-name.z-function.z-destructor { 125 + color: #b7bdf8; 126 } 127 + .z-variable.z-parameter.z-function { 128 + color: #f4dbd6; 129 + font-style: italic; 130 } 131 + .z-keyword.z-declaration.z-function { 132 + color: #ee99a0; 133 + font-style: italic; 134 } 135 + .z-support.z-function { 136 + color: #91d7e3; 137 } 138 + .z-support.z-constant { 139 + color: #8aadf4; 140 } 141 + .z-support.z-type, 142 + .z-support.z-class { 143 + color: #8aadf4; 144 + font-style: italic; 145 } 146 + .z-variable.z-function { 147 + color: #8aadf4; 148 + font-style: italic; 149 } 150 + .z-variable.z-parameter { 151 + color: #f4dbd6; 152 + font-style: italic; 153 } 154 + .z-variable.z-other { 155 + color: #cad3f5; 156 + font-style: italic; 157 } 158 + .z-variable.z-other.z-member { 159 + color: #f4dbd6; 160 } 161 + .z-variable.z-language { 162 + color: #8bd5ca; 163 } 164 + .z-entity.z-name.z-tag { 165 + color: #f5a97f; 166 } 167 + .z-entity.z-other.z-attribute-name { 168 + color: #c6a0f6; 169 + font-style: italic; 170 } 171 + .z-punctuation.z-definition.z-tag { 172 + color: #ee99a0; 173 } 174 + .z-markup.z-underline.z-link.z-markdown { 175 + color: #f4dbd6; 176 + text-decoration: underline; 177 + font-style: italic; 178 } 179 + .z-meta.z-link.z-inline.z-description { 180 + color: #b7bdf8; 181 + font-weight: bold; 182 } 183 + .z-comment.z-block.z-markdown, 184 + .z-meta.z-code-fence, 185 + .z-markup.z-raw.z-code-fence, 186 + .z-markup.z-raw.z-inline { 187 + color: #8bd5ca; 188 + font-style: italic; 189 } 190 + .z-punctuation.z-definition.z-heading, 191 + .z-entity.z-name.z-section { 192 + color: #8aadf4; 193 + font-weight: bold; 194 } 195 + .z-markup.z-italic { 196 + color: #ee99a0; 197 + font-style: italic; 198 } 199 + .z-markup.z-bold { 200 + color: #ee99a0; 201 + font-weight: bold; 202 } 203 + .z-constant.z-character.z-escape { 204 + color: #f5bde6; 205 } 206 + html[data-theme="dark"] 207 + .z-source.z-shell.z-bash 208 + .z-meta.z-function.z-shell 209 + .z-meta.z-compound.z-shell 210 + .z-meta.z-function-call.z-identifier.z-shell { 211 + color: #f5bde6; 212 } 213 + .z-variable.z-language.z-shell { 214 + color: #ed8796; 215 + font-style: italic; 216 } 217 218 + .z-source.z-lua 219 + .z-meta.z-function.z-lua 220 + .z-meta.z-block.z-lua 221 + .z-meta.z-mapping.z-value.z-lua 222 + .z-meta.z-mapping.z-key.z-lua 223 + .z-string.z-unquoted.z-key.z-lua { 224 + color: #b7bdf8; 225 + font-style: italic; 226 } 227 228 + .z-source.z-lua 229 + .z-meta.z-function.z-lua 230 + .z-meta.z-block.z-lua 231 + .z-meta.z-mapping.z-key.z-lua 232 + .z-string.z-unquoted.z-key.z-lua { 233 + color: #f0c6c6; 234 } 235 + .z-entity.z-name.z-constant.z-java { 236 + color: #8bd5ca; 237 } 238 + .z-support.z-type.z-property-name.z-css { 239 + color: #f0c6c6; 240 + font-style: italic; 241 } 242 + .z-support.z-constant.z-property-value.z-css { 243 + color: #cad3f5; 244 } 245 + .z-constant.z-numeric.z-suffix.z-css, 246 + .z-keyword.z-other.z-unit.z-css { 247 + color: #8bd5ca; 248 + font-style: italic; 249 } 250 + .z-variable.z-other.z-custom-property.z-name.z-css, 251 + .z-support.z-type.z-custom-property.z-name.z-css, 252 + .z-punctuation.z-definition.z-custom-property.z-css { 253 + color: #8bd5ca; 254 } 255 + .z-entity.z-name.z-tag.z-css { 256 + color: #b7bdf8; 257 } 258 + .z-variable.z-other.z-sass { 259 + color: #8bd5ca; 260 } 261 + .z-invalid { 262 + color: #cad3f5; 263 + background-color: #ed8796; 264 } 265 + .z-invalid.z-deprecated { 266 + color: #cad3f5; 267 + background-color: #c6a0f6; 268 } 269 + .z-meta.z-diff, 270 + .z-meta.z-diff.z-header { 271 + color: #6e738d; 272 } 273 + .z-markup.z-deleted { 274 + color: #ed8796; 275 } 276 + .z-markup.z-inserted { 277 + color: #a6da95; 278 } 279 + .z-markup.z-changed { 280 + color: #eed49f; 281 } 282 + .z-message.z-error { 283 + color: #ed8796; 284 }
+20
scripts/build.ts
···
··· 1 + #!/usr/bin/env bun 2 + 3 + import { existsSync } from 'fs'; 4 + 5 + await Bun.$`rm -rf .zola-build`.quiet(); 6 + await Bun.$`mkdir -p .zola-build`.quiet(); 7 + await Bun.$`cp -r content .zola-build/`.quiet(); 8 + 9 + const optionalDirs = ['static', 'templates', 'sass', 'syntaxes']; 10 + for (const dir of optionalDirs) { 11 + if (existsSync(dir)) { 12 + await Bun.$`cp -r ${dir} .zola-build/`.quiet(); 13 + } 14 + } 15 + 16 + await Bun.$`cp config.toml .zola-build/`.quiet(); 17 + await Bun.$`bun run scripts/preprocess.ts .zola-build/content`.quiet(); 18 + await Bun.$`cd .zola-build && zola build --force --output-dir ../public`; 19 + await Bun.$`rm -rf .zola-build`.quiet(); 20 +
+102
scripts/dev.ts
···
··· 1 + #!/usr/bin/env bun 2 + 3 + import { watch } from 'fs'; 4 + import { existsSync, mkdirSync, copyFileSync, unlinkSync, rmSync } from 'fs'; 5 + import { spawn } from 'child_process'; 6 + import { dirname, join } from 'path'; 7 + 8 + let zolaProcess: any = null; 9 + 10 + function cleanup() { 11 + if (zolaProcess) { 12 + zolaProcess.kill(); 13 + } 14 + rmSync('.zola-build', { recursive: true, force: true }); 15 + process.exit(0); 16 + } 17 + 18 + process.on('SIGINT', cleanup); 19 + process.on('SIGTERM', cleanup); 20 + 21 + function ensureDir(filePath: string) { 22 + const dir = dirname(filePath); 23 + if (!existsSync(dir)) { 24 + mkdirSync(dir, { recursive: true }); 25 + } 26 + } 27 + 28 + function copyFile(src: string, dest: string) { 29 + ensureDir(dest); 30 + copyFileSync(src, dest); 31 + } 32 + 33 + function deleteFile(dest: string) { 34 + if (existsSync(dest)) { 35 + unlinkSync(dest); 36 + } 37 + } 38 + 39 + async function initialSync() { 40 + await Bun.$`rm -rf .zola-build`.quiet(); 41 + await Bun.$`mkdir -p .zola-build`.quiet(); 42 + await Bun.$`cp -r content .zola-build/`.quiet(); 43 + 44 + const optionalDirs = ['static', 'templates', 'sass', 'syntaxes']; 45 + for (const dir of optionalDirs) { 46 + if (existsSync(dir)) { 47 + await Bun.$`cp -r ${dir} .zola-build/`.quiet(); 48 + } 49 + } 50 + 51 + await Bun.$`cp config.toml .zola-build/`.quiet(); 52 + await Bun.$`bun run scripts/preprocess.ts .zola-build/content`.quiet(); 53 + } 54 + 55 + async function handleFileChange(dir: string, filename: string) { 56 + const srcPath = join(dir, filename); 57 + const destPath = join('.zola-build', dir, filename); 58 + 59 + if (existsSync(srcPath)) { 60 + copyFile(srcPath, destPath); 61 + if (dir === 'content' && filename.endsWith('.md')) { 62 + await Bun.$`bun run scripts/preprocess.ts ${destPath}`.quiet(); 63 + } 64 + } else { 65 + deleteFile(destPath); 66 + } 67 + } 68 + 69 + async function handleConfigChange() { 70 + copyFile('config.toml', '.zola-build/config.toml'); 71 + } 72 + 73 + async function startServer() { 74 + await initialSync(); 75 + 76 + zolaProcess = spawn('zola', ['serve', '--force', '--interface', '0.0.0.0', '--output-dir', '../public'], { 77 + cwd: '.zola-build', 78 + stdio: 'inherit' 79 + }); 80 + 81 + zolaProcess.on('error', (err: Error) => { 82 + console.error('Failed to start Zola:', err); 83 + }); 84 + } 85 + 86 + await startServer(); 87 + 88 + const watchDirs = ['content', 'templates', 'sass', 'static', 'syntaxes']; 89 + 90 + for (const dir of watchDirs) { 91 + if (existsSync(dir)) { 92 + watch(dir, { recursive: true }, (event, filename) => { 93 + if (filename && !filename.includes('.zola-build')) { 94 + handleFileChange(dir, filename); 95 + } 96 + }); 97 + } 98 + } 99 + 100 + watch('config.toml', () => { 101 + handleConfigChange(); 102 + });
+122
scripts/genOG.ts
···
··· 1 + import puppeteer from "puppeteer"; 2 + import { readdir, mkdir } from "node:fs/promises"; 3 + 4 + const template = await Bun.file("tools/og.html").text(); 5 + 6 + const browser = await puppeteer.launch({ 7 + args: ["--no-sandbox"], 8 + executablePath: process.env.PUPPETEER_EXEC_PATH, // set by docker container 9 + }); 10 + 11 + async function og( 12 + postname: string, 13 + type: string, 14 + by: string | undefined, 15 + outputPath: string, 16 + width = 1200, 17 + height = 630, 18 + ) { 19 + const page = await browser.newPage(); 20 + 21 + await page.setViewport({ width, height }); 22 + 23 + await page.setContent( 24 + template 25 + .toString() 26 + .replace("{{postname}}", postname) 27 + .replace("{{type}}", type) 28 + .replace("{{by}}", by || ""), 29 + ); 30 + 31 + await page.screenshot({ path: outputPath }); 32 + } 33 + 34 + async function fileExists(path: string): Promise<boolean> { 35 + try { 36 + await Bun.file(path); 37 + return true; 38 + } catch (e) { 39 + return false; 40 + } 41 + } 42 + 43 + try { 44 + // check if the public/blog folder exists 45 + // if not exit 46 + // if it does, get all the folders and then get the title tag from the index.html 47 + 48 + if (!(await fileExists("public/"))) { 49 + console.error("public/ does not exist"); 50 + process.exit(1); 51 + } 52 + 53 + // read all the files in the current directory filtering for index.htmls 54 + const files = (await readdir("public/", { recursive: true })).filter((file) => 55 + file.endsWith("index.html"), 56 + ); 57 + 58 + const directories = new Set( 59 + files.map((file) => file.replace("index.html", "")), 60 + ); 61 + 62 + const existing = (await readdir("static/")).filter((file) => 63 + directories.has(file), 64 + ); 65 + 66 + // create not existing 67 + for (const dir of directories) { 68 + if (!existing.includes(dir)) { 69 + await mkdir(`static/${dir.split("/").slice(0, -1).join("/")}`, { 70 + recursive: true, 71 + }); 72 + } 73 + } 74 + 75 + console.log("Generating OG images for", files.length, "files"); 76 + 77 + // for each file, get the title tag from the index.html 78 + for (const file of files) { 79 + const index = await Bun.file(`public/${file}`).text(); 80 + const title = index.match(/<title>(.*?)<\/title>/)[1]; 81 + let type = "Page"; 82 + let by: string | undefined; 83 + switch (file.split("/")[0]) { 84 + case "blog": 85 + type = "Blog"; 86 + if (file.split("/")[1] !== "index.html") { 87 + by = "<p>A post ... yeah thats about it</p>"; 88 + } else { 89 + by = "<p>All authored by me ... or are they???</p>"; 90 + } 91 + break; 92 + case "verify": 93 + type = "Slash Page"; 94 + by = "<p>So you can stalk me ๐Ÿ’€</p>"; 95 + break; 96 + case "pfp": 97 + type = "Slash Page"; 98 + by = "<p>Want to stare at my pretty face?</p>"; 99 + break; 100 + case "tags": 101 + if (file.split("/")[1] === "index.html") { 102 + type = "Tags"; 103 + by = "<p>A total archive!</p>"; 104 + } else { 105 + type = "Tag"; 106 + by = "<p>Find more posts like this!</p>"; 107 + } 108 + break; 109 + case "index.html": 110 + type = "Root"; 111 + by = "<p>Where it all begins</p>"; 112 + break; 113 + } 114 + 115 + console.log("Generating OG for", file, "title:", title, "with type:", type); 116 + await og(title, type, by, `static/${file.replace("index.html", "og.png")}`); 117 + } 118 + } catch (e) { 119 + console.error(e); 120 + } finally { 121 + await browser.close(); 122 + }
+111
scripts/og.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <style> 5 + :root, 6 + ::backdrop { 7 + color-scheme: dark; 8 + --bg: #222529; 9 + --bg-light: #464949; 10 + --text: #d6d6d6; 11 + --text-light: #c5c0b7; 12 + --accent: #78b6ad; 13 + --accent-light: #87c9e5; 14 + --accent-text: var(--bg); 15 + --border: #dbd5bc; 16 + --link: #e2c8a2; 17 + --gradient-average-light: oklch(86.49% 0.018 73.05); 18 + --gradient-average-dark: oklch(27.58% 0.0203 289.13); 19 + --nightshade-violet: oklch(22.96% 0.0242 287.67); 20 + --purple-night: oklch(18.96% 0.0242 287.67); 21 + --dark-crushed-grape: oklch(74.02% 0.0756 311.96); 22 + --light-crushed-grape: oklch(73.48% 0.1008 284.99); 23 + --reseda-green: oklch(62.33% 0.0475 126.94); 24 + --earth-yellow: oklch(87.45% 0.0203 74.93); 25 + --sunset: oklch(87.45% 0.0334 74.93); 26 + --ultra-violet: oklch(42.21% 0.0676 297.45); 27 + --rose-quartz: oklch(65.32% 0.0585 311.96); 28 + --pink-puree: oklch(75.65% 0.0555 290.76); 29 + --lavendar-breeze: oklch(91.06% 0.0223 290.76); 30 + --purple-gray: oklch(25.63% 0.0002 290.76); 31 + --alice-blue: oklch(95.38% 0.0118 239.91); 32 + } 33 + 34 + body { 35 + font-weight: 600; 36 + color: var(--lavendar-breeze); 37 + background-color: var(--purple-night); 38 + font-family: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", 39 + monospace; 40 + display: flex; 41 + flex-direction: column; 42 + text-align: center; 43 + } 44 + 45 + div { 46 + margin: 0; 47 + display: flex; 48 + flex-direction: column; 49 + align-items: center; 50 + justify-content: center; 51 + height: 90vh; /* 90% of viewport height */ 52 + width: 90vw; /* 90% of viewport width */ 53 + padding: 5vh 5vw; /* 5% border on all sides */ 54 + box-sizing: border-box; 55 + align-self: center; 56 + } 57 + 58 + h1 { 59 + font-size: calc(2 * 2vw); 60 + } 61 + h2 { 62 + font-size: calc(1.75 * 2vw); 63 + } 64 + h3 { 65 + font-size: calc(1.5 * 2vw); 66 + } 67 + h4 { 68 + font-size: calc(1.25 * 2vw); 69 + } 70 + h5 { 71 + font-size: calc(1 * 2vw); 72 + } 73 + h6 { 74 + font-size: calc(0.75 * 2vw); 75 + } 76 + 77 + h1, 78 + h2, 79 + h3, 80 + h4, 81 + h5, 82 + h6 { 83 + margin: 0.5em 0 0.5em 0; 84 + padding: 0.22em 0.4em 0.22em 0.4em; 85 + border-radius: 0.1em; 86 + width: fit-content; 87 + color: var(--lavendar-breeze); 88 + } 89 + 90 + h1 { 91 + background-color: var(--rose-quartz); 92 + color: var(--purple-gray); 93 + } 94 + 95 + p { 96 + margin: 1rem 0; 97 + color: var(--pink-puree); 98 + font-size: calc( 99 + 1rem + 1vw 100 + ); /* Adjust font size based on viewport width */ 101 + } 102 + </style> 103 + </head> 104 + <body> 105 + <div> 106 + <h1>{{type}}</h1> 107 + <h2>{{postname}}</h2> 108 + {{by}} 109 + </div> 110 + </body> 111 + </html>
+144
scripts/preprocess.ts
···
··· 1 + #!/usr/bin/env bun 2 + 3 + import fs from 'fs'; 4 + import path from 'path'; 5 + import { glob } from 'glob'; 6 + 7 + const contentDir = process.argv[2] || 'content'; 8 + 9 + function splitByCodeBlocks(content: string): { text: string; isCode: boolean }[] { 10 + const parts: { text: string; isCode: boolean }[] = []; 11 + const codeBlockRegex = /^(```|~~~)/gm; 12 + 13 + let lastIndex = 0; 14 + let inCodeBlock = false; 15 + let match; 16 + 17 + codeBlockRegex.lastIndex = 0; 18 + 19 + while ((match = codeBlockRegex.exec(content)) !== null) { 20 + const segment = content.slice(lastIndex, match.index); 21 + if (segment) { 22 + parts.push({ text: segment, isCode: inCodeBlock }); 23 + } 24 + inCodeBlock = !inCodeBlock; 25 + lastIndex = match.index; 26 + } 27 + 28 + // Add remaining content 29 + if (lastIndex < content.length) { 30 + parts.push({ text: content.slice(lastIndex), isCode: inCodeBlock }); 31 + } 32 + 33 + return parts; 34 + } 35 + 36 + function transformCallouts(content: string): string { 37 + return content.replace( 38 + /^> \[!(INFO|WARNING|WARN|DANGER|ERROR|TIP|HINT|NOTE)\]\n((?:> .*\n?)*)/gm, 39 + (match, type, body) => { 40 + const cleanBody = body.replace(/^> /gm, '').trim(); 41 + const normalizedType = type.toLowerCase() === 'warn' ? 'warning' : 42 + type.toLowerCase() === 'error' ? 'danger' : 43 + type.toLowerCase() === 'hint' ? 'tip' : 44 + type.toLowerCase(); 45 + return `{% callout(type="${normalizedType}") %}\n${cleanBody}\n{% end %}\n`; 46 + } 47 + ); 48 + } 49 + 50 + function transformImages(content: string): string { 51 + // Transform multiple images: !![alt1](url1)[alt2](url2){attrs} 52 + content = content.replace( 53 + /!!(\[([^\]]*)\]\(([^)]+)\))+(?:\{([^}]+)\})?/g, 54 + (match) => { 55 + // Extract all [alt](url) pairs 56 + const pairs = [...match.matchAll(/\[([^\]]*)\]\(([^)]+)\)/g)]; 57 + const urls = pairs.map(p => p[2]).join(', '); 58 + const alts = pairs.map(p => p[1]).join(', '); 59 + 60 + // Extract attrs if present 61 + const attrsMatch = match.match(/\{([^}]+)\}$/); 62 + const attrs = attrsMatch ? attrsMatch[1] : ''; 63 + 64 + const params: string[] = [`id="${urls}"`]; 65 + 66 + if (alts.trim()) { 67 + params.push(`alt="${alts}"`); 68 + } 69 + 70 + if (attrs) { 71 + const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || []; 72 + if (classes.length) { 73 + params.push(`class="${classes.join(' ')}"`); 74 + } 75 + 76 + const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g); 77 + for (const [, key, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 78 + if (key !== 'class') { 79 + const value = doubleQuoted || singleQuoted || unquoted; 80 + params.push(`${key}="${value}"`); 81 + } 82 + } 83 + } 84 + 85 + return `{{ imgs(${params.join(', ')}) }}`; 86 + } 87 + ); 88 + 89 + // Transform single images: ![alt](url){attrs} 90 + content = content.replace( 91 + /!\[([^\]]*)\]\(([^)]+)\)(?:\{([^}]+)\})?/g, 92 + (match, alt, url, attrs) => { 93 + const params: string[] = [`id="${url}"`]; 94 + 95 + if (alt) { 96 + params.push(`alt="${alt}"`); 97 + } 98 + 99 + if (attrs) { 100 + const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || []; 101 + if (classes.length) { 102 + params.push(`class="${classes.join(' ')}"`); 103 + } 104 + 105 + const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g); 106 + for (const [, key, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 107 + if (key !== 'class') { 108 + const value = doubleQuoted || singleQuoted || unquoted; 109 + params.push(`${key}="${value}"`); 110 + } 111 + } 112 + } 113 + 114 + return `{{ img(${params.join(', ')}) }}`; 115 + } 116 + ); 117 + 118 + return content; 119 + } 120 + 121 + function processFile(filePath: string): void { 122 + let content = fs.readFileSync(filePath, 'utf8'); 123 + const originalContent = content; 124 + 125 + // Split by code blocks and only transform non-code parts 126 + const parts = splitByCodeBlocks(content); 127 + content = parts.map(part => { 128 + if (part.isCode) { 129 + return part.text; // Don't transform code blocks 130 + } 131 + let text = part.text; 132 + text = transformCallouts(text); 133 + text = transformImages(text); 134 + return text; 135 + }).join(''); 136 + 137 + if (content !== originalContent) { 138 + fs.writeFileSync(filePath, content); 139 + } 140 + } 141 + 142 + const files = glob.sync(`${contentDir}/**/*.md`); 143 + files.forEach(processFile); 144 +
+180
scripts/rehost-cdn.sh
···
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + API_URL="https://cdn.hackclub.com/api/v3/new" 5 + TOKEN="${HACKCLUB_CDN_TOKEN:-}" 6 + if [[ -z "${TOKEN}" ]]; then 7 + TOKEN="${1:-}" 8 + fi 9 + if [[ -z "${TOKEN}" ]]; then 10 + echo "Usage: HACKCLUB_CDN_TOKEN=... $0 [token] [--dry-run] [paths...]" >&2 11 + exit 1 12 + fi 13 + 14 + DRY_RUN=false 15 + SKIP_CHECK=false 16 + CACHED_URLS=() 17 + ARGS=() 18 + for a in "$@"; do 19 + case "$a" in 20 + --dry-run) DRY_RUN=true ;; 21 + --skip-check) SKIP_CHECK=true ;; 22 + *) ARGS+=("$a") ;; 23 + esac 24 + done 25 + # remove token if passed as first arg 26 + if [[ ${#ARGS[@]} -gt 0 && "${ARGS[0]}" != "--dry-run" ]]; then 27 + ARGS=("${ARGS[@]:1}") 28 + fi 29 + 30 + PATHS=("content") 31 + if [[ ${#ARGS[@]} -gt 0 ]]; then PATHS=("${ARGS[@]}"); fi 32 + 33 + TMP_DIR=".crush/rehost-cdn" 34 + MAP_FILE="$TMP_DIR/map.tsv" 35 + mkdir -p "$TMP_DIR" 36 + touch "$MAP_FILE" 37 + 38 + collect_urls() { 39 + # Markdown images: ![alt](URL) 40 + rg -n --no-heading -e '!\[[^\]]*\]\((https?://[^)\s]+)\)' -g '!**/*.map' -g '!**/*.lock' "${PATHS[@]}" 2>/dev/null | 41 + awk -F: '{file=$1; sub(/^[^:]*:/, "", $0); match($0, /!\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/, m); if(m[1]!="") print file"\t"m[1]}' | 42 + # Zola shortcode variants: 43 + # - {% img(id="URL", ...) %} 44 + # - {{ img(id="URL", ...) }} 45 + cat <( rg -n --no-heading -e '\{[%{]\s*img[^}%]*[}%]\}' "${PATHS[@]}" 2>/dev/null | \ 46 + awk -F: '{file=$1; sub(/^[^:]*:/, "", $0); if (match($0, /(id|src)[[:space:]]*=[[:space:]]*"(https?:\/\/[^"[:space:]]+)"/, m)) print file"\t"m[2]}' ) | 47 + awk -F'\t' '{print $1"\t"$2}' | 48 + grep -E '\.(png|jpe?g|gif|webp|svg|bmp|tiff?|avif)(\?.*)?$' -i | 49 + grep -vE 'hc-cdn\.|cdn\.hackclub\.com' 50 + } 51 + 52 + batch_upload() { 53 + payload=$(jq -sR 'split("\n")|map(select(length>0))' <(printf "%s\n" "$@")) 54 + for attempt in 1 2 3; do 55 + resp=$(curl -sS -w "\n%{http_code}" -X POST "$API_URL" \ 56 + -H "Authorization: Bearer $TOKEN" \ 57 + -H 'Content-Type: application/json' \ 58 + --data-raw "$payload" 2>&1) || true 59 + body=$(printf "%s" "$resp" | sed '$d') 60 + code=$(printf "%s" "$resp" | tail -n1) 61 + if [[ "$code" == "200" ]]; then 62 + printf "%s" "$body" | jq -r '.files[] | .sourceUrl? as $s | .deployedUrl + "\t" + ( $s // "" )' 63 + return 0 64 + fi 65 + echo "Upload attempt $attempt failed with $code" >&2 66 + echo "Response body:" >&2 67 + printf "%s\n" "$body" >&2 68 + echo "Payload:" >&2 69 + printf "%s\n" "$payload" >&2 70 + sleep $((attempt*2)) 71 + done 72 + echo "Upload failed after retries" >&2 73 + return 1 74 + } 75 + 76 + mapfile -t LINES < <(collect_urls | sort -u) 77 + 78 + URLS_TO_SEND=() 79 + FILES=() 80 + total=${#LINES[@]} 81 + idx=0 82 + for line in "${LINES[@]}"; do 83 + idx=$((idx+1)) 84 + file="${line%%$'\t'*}" 85 + url="${line#*$'\t'}" 86 + if grep -Fq "${url}" "$MAP_FILE" 2>/dev/null; then 87 + echo "[$idx/$total] cached: $url -> will rewrite only" 88 + CACHED_URLS+=("$url") 89 + continue 90 + fi 91 + if $DRY_RUN; then 92 + echo "[$idx/$total] queued: $url" 93 + URLS_TO_SEND+=("$url") 94 + FILES+=("$file") 95 + else 96 + if $SKIP_CHECK; then 97 + echo "[$idx/$total] no-check: $url" 98 + URLS_TO_SEND+=("$url") 99 + FILES+=("$file") 100 + else 101 + echo -n "[$idx/$total] checking: $url ... " 102 + code=$(curl -sS -o /dev/null -w '%{http_code}' -L "$url" || echo 000) 103 + if [[ "$code" =~ ^2|3 ]]; then 104 + echo "ok ($code)" 105 + URLS_TO_SEND+=("$url") 106 + FILES+=("$file") 107 + else 108 + echo "fail ($code)"; echo "Skipping: $url" >&2 109 + fi 110 + fi 111 + fi 112 + done 113 + 114 + if [[ ${#URLS_TO_SEND[@]} -eq 0 ]]; then 115 + echo "No new image URLs to process"; exit 0 116 + fi 117 + 118 + BATCH=50 119 + start=0 120 + # Rewrites for cached URLs from map without uploading 121 + if [ "${#CACHED_URLS[@]}" -gt 0 ] 2>/dev/null; then 122 + echo "Rewriting cached URLs from map without upload..." 123 + for src in "${CACHED_URLS[@]}"; do 124 + dst=$(awk -F'\t' -v s="$src" '$1==s{print $2}' "$MAP_FILE" | head -n1) 125 + [[ -z "$dst" ]] && continue 126 + rg -l --fixed-strings -- "$src" "${PATHS[@]}" 2>/dev/null | while read -r f; do 127 + mkdir -p "$TMP_DIR/backup" 128 + if [[ ! -e "$TMP_DIR/backup/$f" ]]; then 129 + mkdir -p "$TMP_DIR/backup/$(dirname "$f")" 130 + cp "$f" "$TMP_DIR/backup/$f" 131 + fi 132 + sed -i "s#$(printf '%s' "$src" | sed -e 's/[.[\*^$]/\\&/g' -e 's#/#\\/#g')#$(printf '%s' "$dst" | sed -e 's/[&]/\\&/g' -e 's#/#\\/#g')#g" "$f" 133 + echo "Rewrote (cached): $f" 134 + done 135 + done 136 + fi 137 + 138 + while [[ $start -lt ${#URLS_TO_SEND[@]} ]]; do 139 + end=$(( start + BATCH )) 140 + if [[ $end -gt ${#URLS_TO_SEND[@]} ]]; then end=${#URLS_TO_SEND[@]}; fi 141 + chunk=("${URLS_TO_SEND[@]:start:end-start}") 142 + if $DRY_RUN; then 143 + for u in "${chunk[@]}"; do echo "DRY: would upload $u"; done 144 + else 145 + echo "Uploading ${#chunk[@]} URLs..." 146 + resp=$(batch_upload "${chunk[@]}") || { echo "Upload failed" >&2; exit 1; } 147 + echo "Upload response:"; printf "%s\n" "$resp" 148 + mapfile -t deployed_arr < <(printf "%s\n" "$resp" | awk '{print $0}') 149 + for i in "${!chunk[@]}"; do 150 + src="${chunk[$i]}" 151 + dst="${deployed_arr[$i]:-}" 152 + if [[ -n "$dst" ]]; then 153 + printf "%s\t%s\n" "$src" "$dst" | tee -a "$MAP_FILE" 154 + fi 155 + done 156 + fi 157 + start=$end 158 + done 159 + 160 + if $DRY_RUN; then echo "DRY: skipping replacements"; exit 0; fi 161 + 162 + # Replace in-place using map 163 + if [[ -s "$MAP_FILE" ]]; then 164 + cp "$MAP_FILE" "$TMP_DIR/map-$(date +%s).tsv" 165 + while IFS=$'\t' read -r src dst; do 166 + [[ -z "$src" || -z "$dst" ]] && continue 167 + rg -l --fixed-strings -- "$src" "${PATHS[@]}" 2>/dev/null | while read -r f; do 168 + mkdir -p "$TMP_DIR/backup" 169 + if [[ ! -e "$TMP_DIR/backup/$f" ]]; then 170 + mkdir -p "$TMP_DIR/backup/$(dirname "$f")" 171 + cp "$f" "$TMP_DIR/backup/$f" 172 + fi 173 + sed -i "s#$(printf '%s' "$src" | sed -e 's/[.[\*^$]/\\&/g' -e 's#/#\\/#g')#$(printf '%s' "$dst" | sed -e 's/[&]/\\&/g' -e 's#/#\\/#g')#g" "$f" 174 + echo "Rewrote: $f" 175 + done 176 + done < "$MAP_FILE" 177 + echo "Backups in $TMP_DIR/backup" 178 + fi 179 + 180 + echo "Done"
+320
scripts/rehost.ts
···
··· 1 + #!/usr/bin/env bun 2 + 3 + import fs from "fs"; 4 + import path from "path"; 5 + import { glob } from "glob"; 6 + import cliProgress from "cli-progress"; 7 + import sharp from "sharp"; 8 + 9 + const UPLOAD_URL = "https://l4.dunkirk.sh/upload"; 10 + const AUTH_TOKEN = "crumpets"; 11 + const contentDir = process.argv[2] || "content"; 12 + const CONCURRENCY = 15; // Number of parallel uploads 13 + const MAX_DIMENSION = 1920; // Max dimension for either width or height 14 + 15 + interface ImageMatch { 16 + filePath: string; 17 + originalUrl: string; 18 + line: number; 19 + } 20 + 21 + interface UploadResult { 22 + url: string; 23 + newUrl: string | null; 24 + error?: string; 25 + } 26 + 27 + async function uploadImage( 28 + url: string, 29 + progressBar?: cliProgress.SingleBar, 30 + ): Promise<{ newUrl: string | null; error?: string }> { 31 + try { 32 + const response = await fetch(url, { 33 + signal: AbortSignal.timeout(30000), // 30 second timeout 34 + }); 35 + 36 + if (!response.ok) { 37 + progressBar?.increment(); 38 + return { 39 + newUrl: null, 40 + error: `Download failed: ${response.status} ${response.statusText}`, 41 + }; 42 + } 43 + 44 + const blob = await response.blob(); 45 + let buffer = Buffer.from(await blob.arrayBuffer()); 46 + 47 + // Get file extension from URL or content type 48 + const urlExt = url.split(".").pop()?.split("?")[0]; 49 + const contentType = response.headers.get("content-type") || ""; 50 + let ext = urlExt || "jpg"; 51 + 52 + if (contentType.includes("png")) ext = "png"; 53 + else if (contentType.includes("jpeg") || contentType.includes("jpg")) 54 + ext = "jpg"; 55 + else if (contentType.includes("gif")) ext = "gif"; 56 + else if (contentType.includes("webp")) ext = "webp"; 57 + 58 + // Resize image if it's too large 59 + try { 60 + const image = sharp(buffer); 61 + const metadata = await image.metadata(); 62 + 63 + if (metadata.width && metadata.height) { 64 + const maxDimension = Math.max(metadata.width, metadata.height); 65 + 66 + if (maxDimension > MAX_DIMENSION) { 67 + // Resize so the longest side is MAX_DIMENSION 68 + const isLandscape = metadata.width > metadata.height; 69 + buffer = await image 70 + .resize( 71 + isLandscape ? MAX_DIMENSION : undefined, 72 + isLandscape ? undefined : MAX_DIMENSION, 73 + { 74 + fit: 'inside', 75 + withoutEnlargement: true, 76 + } 77 + ) 78 + .toBuffer(); 79 + } 80 + } 81 + } catch (resizeError) { 82 + // If resize fails, continue with original buffer 83 + console.error(`\nWarning: Failed to resize ${url}:`, resizeError); 84 + } 85 + 86 + const filename = `image_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`; 87 + 88 + // Create form data 89 + const formData = new FormData(); 90 + const file = new File([buffer], filename, { type: contentType }); 91 + formData.append("file", file); 92 + 93 + const uploadResponse = await fetch(UPLOAD_URL, { 94 + method: "POST", 95 + headers: { 96 + Authorization: `Bearer ${AUTH_TOKEN}`, 97 + }, 98 + body: formData, 99 + signal: AbortSignal.timeout(30000), 100 + }); 101 + 102 + if (!uploadResponse.ok) { 103 + const errorText = await uploadResponse.text(); 104 + progressBar?.increment(); 105 + return { 106 + newUrl: null, 107 + error: `Upload failed: ${uploadResponse.status} ${uploadResponse.statusText} - ${errorText}`, 108 + }; 109 + } 110 + 111 + const result = await uploadResponse.json(); 112 + progressBar?.increment(); 113 + return { newUrl: result.url }; 114 + } catch (error) { 115 + progressBar?.increment(); 116 + return { 117 + newUrl: null, 118 + error: `Exception: ${error instanceof Error ? error.message : String(error)}`, 119 + }; 120 + } 121 + } 122 + 123 + async function processInBatches<T, R>( 124 + items: T[], 125 + batchSize: number, 126 + processor: (item: T) => Promise<R>, 127 + ): Promise<R[]> { 128 + const results: R[] = []; 129 + 130 + for (let i = 0; i < items.length; i += batchSize) { 131 + const batch = items.slice(i, i + batchSize); 132 + const batchResults = await Promise.all(batch.map(processor)); 133 + results.push(...batchResults); 134 + } 135 + 136 + return results; 137 + } 138 + 139 + function findImages(filePath: string): ImageMatch[] { 140 + const content = fs.readFileSync(filePath, "utf8"); 141 + const lines = content.split("\n"); 142 + const images: ImageMatch[] = []; 143 + 144 + for (let i = 0; i < lines.length; i++) { 145 + const line = lines[i]; 146 + 147 + // Find all image URLs in standard markdown: ![alt](url) or ![alt](url){attrs} 148 + const singleImageRegex = /!\[([^\]]*)\]\(([^)]+)\)(?:\{[^}]+\})?/g; 149 + let match; 150 + 151 + while ((match = singleImageRegex.exec(line)) !== null) { 152 + const url = match[2]; 153 + // Only process hel1 cdn URLs, skip gifs 154 + if ( 155 + url.includes("hc-cdn.hel1.your-objectstorage.com") && 156 + !url.toLowerCase().endsWith(".gif") 157 + ) { 158 + images.push({ 159 + filePath, 160 + originalUrl: url, 161 + line: i + 1, 162 + }); 163 + } 164 + } 165 + 166 + // Find all image URLs in multi-image format: !![alt1](url1)[alt2](url2){attrs} 167 + const multiImageRegex = /!!(\[([^\]]*)\]\(([^)]+)\))+(?:\{[^}]+\})?/g; 168 + while ((match = multiImageRegex.exec(line)) !== null) { 169 + const urlMatches = [...match[0].matchAll(/\[([^\]]*)\]\(([^)]+)\)/g)]; 170 + for (const urlMatch of urlMatches) { 171 + const url = urlMatch[2]; 172 + // Only process hel1 cdn URLs, skip gifs 173 + if ( 174 + url.includes("hc-cdn.hel1.your-objectstorage.com") && 175 + !url.toLowerCase().endsWith(".gif") 176 + ) { 177 + images.push({ 178 + filePath, 179 + originalUrl: url, 180 + line: i + 1, 181 + }); 182 + } 183 + } 184 + } 185 + } 186 + 187 + return images; 188 + } 189 + 190 + function replaceImageUrl( 191 + filePath: string, 192 + oldUrl: string, 193 + newUrl: string, 194 + ): void { 195 + let content = fs.readFileSync(filePath, "utf8"); 196 + 197 + // Replace all occurrences of the old URL with the new one 198 + // This handles both single and multi-image formats 199 + content = content.replaceAll(oldUrl, newUrl); 200 + 201 + fs.writeFileSync(filePath, content); 202 + } 203 + 204 + async function main() { 205 + const files = glob.sync(`${contentDir}/**/*.md`); 206 + const allImages: ImageMatch[] = []; 207 + 208 + // Find all images across all files 209 + for (const file of files) { 210 + const images = findImages(file); 211 + allImages.push(...images); 212 + } 213 + 214 + if (allImages.length === 0) { 215 + console.log("No external images found to rehost."); 216 + return; 217 + } 218 + 219 + const uniqueUrls = [...new Set(allImages.map((img) => img.originalUrl))]; 220 + console.log( 221 + `Found ${uniqueUrls.length} unique images to rehost (${allImages.length} total references)\n`, 222 + ); 223 + 224 + // Create progress bar 225 + const progressBar = new cliProgress.SingleBar({ 226 + format: 227 + "Uploading |{bar}| {percentage}% | {value}/{total} images | ETA: {eta}s", 228 + barCompleteChar: "\u2588", 229 + barIncompleteChar: "\u2591", 230 + hideCursor: true, 231 + }); 232 + 233 + progressBar.start(uniqueUrls.length, 0); 234 + 235 + // Process URLs in parallel with concurrency limit 236 + const urlMap = new Map<string, string>(); 237 + const failedUploads: { url: string; error: string }[] = []; 238 + 239 + const results = await processInBatches( 240 + uniqueUrls, 241 + CONCURRENCY, 242 + async (url) => { 243 + const result = await uploadImage(url, progressBar); 244 + return { url, ...result }; 245 + }, 246 + ); 247 + 248 + progressBar.stop(); 249 + 250 + // Build URL map and collect errors 251 + for (const { url, newUrl, error } of results) { 252 + if (newUrl) { 253 + urlMap.set(url, newUrl); 254 + } else if (error) { 255 + failedUploads.push({ url, error }); 256 + } 257 + } 258 + 259 + const successCount = urlMap.size; 260 + const failCount = uniqueUrls.length - successCount; 261 + 262 + if (failCount > 0) { 263 + console.log( 264 + `\nโš ๏ธ Failed to upload ${failCount} image${failCount === 1 ? "" : "s"}`, 265 + ); 266 + 267 + // Group errors by type 268 + const errorGroups = new Map<string, string[]>(); 269 + for (const { url, error } of failedUploads) { 270 + const errorType = error.split(":")[0]; 271 + if (!errorGroups.has(errorType)) { 272 + errorGroups.set(errorType, []); 273 + } 274 + errorGroups.get(errorType)!.push(url); 275 + } 276 + 277 + console.log("\nError summary:"); 278 + for (const [errorType, urls] of errorGroups.entries()) { 279 + console.log( 280 + ` ${errorType}: ${urls.length} image${urls.length === 1 ? "" : "s"}`, 281 + ); 282 + if (urls.length <= 3) { 283 + urls.forEach((url) => console.log(` - ${url}`)); 284 + } else { 285 + urls.slice(0, 2).forEach((url) => console.log(` - ${url}`)); 286 + console.log(` ... and ${urls.length - 2} more`); 287 + } 288 + } 289 + } 290 + 291 + if (urlMap.size === 0) { 292 + console.log("\nโŒ No images were successfully uploaded."); 293 + return; 294 + } 295 + 296 + // Replace URLs in files 297 + console.log( 298 + `\nโœ“ Successfully uploaded ${successCount} image${successCount === 1 ? "" : "s"}`, 299 + ); 300 + console.log("\nUpdating markdown files..."); 301 + 302 + let filesUpdated = 0; 303 + const updatedFiles = new Set<string>(); 304 + 305 + for (const [oldUrl, newUrl] of urlMap.entries()) { 306 + const affectedImages = allImages.filter( 307 + (img) => img.originalUrl === oldUrl, 308 + ); 309 + for (const img of affectedImages) { 310 + replaceImageUrl(img.filePath, oldUrl, newUrl); 311 + updatedFiles.add(img.filePath); 312 + } 313 + } 314 + 315 + console.log( 316 + `โœ“ Updated ${updatedFiles.size} file${updatedFiles.size === 1 ? "" : "s"}\n`, 317 + ); 318 + } 319 + 320 + main();
static/android-chrome-192x192.png

This is a binary file and will not be displayed.

static/android-chrome-512x512.png

This is a binary file and will not be displayed.

static/apple-touch-icon.png

This is a binary file and will not be displayed.

+1
static/badges/MadeByAHuman_04.svg
···
··· 1 + <?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="88" height="31" viewBox="0 0 88 31"><g id="Background"><rect width="88" height="31" fill="#b39ddb"/></g><g id="_3D"><polygon points="88 0 88 31 0 31 2 29 86 29 86 2 88 0" fill="#263238" opacity=".5"/><polygon points="88 0 86 2 2 2 2 29 0 31 0 0 88 0" fill="#fff" opacity=".5"/></g><g id="Text"><path d="m40.86,9.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.04-.77,1.75-.77.93,0,1.72.4,2.13,1.17.37-.72,1.2-1.17,2.06-1.17,1.39,0,2.44.87,2.44,2.52v3.49h-1.21v-3.31Z" fill="#263238"/><path d="m46.01,6.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m53.31,6.94c.77,0,1.59.37,2.04.92v-2.86h1.24v7.95h-1.24v-.89c-.38.54-1.08.99-2.05.99-1.56,0-2.79-1.26-2.79-3.08s1.24-3.03,2.8-3.03Zm.25,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m60.73,13.05c-1.7,0-2.95-1.2-2.95-3.06s1.2-3.05,2.95-3.05,2.88,1.17,2.88,2.91c0,.2-.01.4-.04.6h-4.52c.09.98.78,1.57,1.69,1.57.75,0,1.17-.37,1.4-.83h1.32c-.33,1.03-1.27,1.86-2.72,1.86Zm-1.68-3.59h3.28c-.02-.91-.74-1.49-1.65-1.49-.83,0-1.49.56-1.62,1.49Z" fill="#263238"/><path d="m70.89,6.94c1.57,0,2.78,1.2,2.78,3.03s-1.22,3.08-2.78,3.08c-.98,0-1.68-.44-2.06-.96v.86h-1.22v-7.95h1.22v2.91c.39-.54,1.13-.98,2.06-.98Zm-.27,1.06c-.91,0-1.79.72-1.79,1.99s.88,2,1.79,2,1.8-.74,1.8-2.02-.88-1.97-1.8-1.97Z" fill="#263238"/><path d="m78.95,7.04h1.27l-3.63,8.7h-1.27l1.2-2.88-2.33-5.82h1.36l1.67,4.51,1.73-4.51Z" fill="#263238"/><path d="m34.48,17.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m42.17,16.01h1.22v2.72c.41-.49,1.07-.78,1.84-.78,1.32,0,2.35.87,2.35,2.52v3.49h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-7.95Z" fill="#263238"/><path d="m54.5,23.96h-1.22v-.71c-.39.5-1.05.79-1.75.79-1.39,0-2.44-.87-2.44-2.52v-3.48h1.21v3.3c0,1.08.59,1.64,1.48,1.64s1.49-.56,1.49-1.64v-3.3h1.22v5.92Z" fill="#263238"/><path d="m64.5,20.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.21v-3.31c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.04-.77,1.75-.77.93,0,1.72.4,2.13,1.17.37-.72,1.2-1.17,2.06-1.17,1.39,0,2.44.87,2.44,2.52v3.49h-1.21v-3.31Z" fill="#263238"/><path d="m69.65,17.94c1,0,1.69.47,2.05.96v-.86h1.24v5.92h-1.24v-.88c-.38.5-1.08.98-2.07.98-1.54,0-2.77-1.26-2.77-3.08s1.24-3.03,2.79-3.03Zm.26,1.06c-.91,0-1.79.69-1.79,1.97s.88,2.02,1.79,2.02,1.79-.72,1.79-2-.87-1.99-1.79-1.99Z" fill="#263238"/><path d="m78.74,20.65c0-1.08-.59-1.64-1.48-1.64s-1.49.56-1.49,1.64v3.31h-1.22v-5.92h1.22v.68c.4-.48,1.05-.77,1.76-.77,1.39,0,2.43.87,2.43,2.52v3.49h-1.21v-3.31Z" fill="#263238"/></g><g id="Image"><circle cx="15.37" cy="15.5" r="10.3" fill="#ffcc80" stroke="#263238" stroke-miterlimit="10" stroke-width="1.5"/><rect x="7.55" y="13.8" width="2.54" height="3.4" rx="1.27" ry="1.27" fill="#263238"/><rect x="20.65" y="13.8" width="2.54" height="3.4" rx="1.27" ry="1.27" fill="#263238"/><path d="m18.44,15.5c0,1.7-1.37,3.07-3.07,3.07s-3.07-1.37-3.07-3.07" fill="none" stroke="#263238" stroke-miterlimit="10" stroke-width="1.5"/></g></svg>
static/badges/get-netscape.gif

This is a binary file and will not be displayed.

static/badges/green-team.gif

This is a binary file and will not be displayed.

static/badges/hackclub.png

This is a binary file and will not be displayed.

static/badges/kagi.gif

This is a binary file and will not be displayed.

static/badges/made-with-neovim.png

This is a binary file and will not be displayed.

static/badges/no-web3.gif

This is a binary file and will not be displayed.

static/badges/powered-by-nix.gif

This is a binary file and will not be displayed.

static/badges/tangled.png

This is a binary file and will not be displayed.

static/blog/adding-a-copy-button/og.png

This is a binary file and will not be displayed.

static/blog/airbuds/og.png

This is a binary file and will not be displayed.

static/blog/analyzing-implications-of-online-safety-legislation/og.png

This is a binary file and will not be displayed.

static/blog/atuin/og.png

This is a binary file and will not be displayed.

static/blog/degraded-zpool-proxmox/og.png

This is a binary file and will not be displayed.

static/blog/exporting-from-plausible/og.png

This is a binary file and will not be displayed.

static/blog/garmin-vivoactive-homeassistant/og.png

This is a binary file and will not be displayed.

static/blog/hilton-tomfoolery/og.png

This is a binary file and will not be displayed.

static/blog/install-truenas-core-proxmox/og.png

This is a binary file and will not be displayed.

static/blog/mega/og.png

This is a binary file and will not be displayed.

static/blog/monaspace-vs-code-install/og.png

This is a binary file and will not be displayed.

static/blog/my-animations/og.png

This is a binary file and will not be displayed.

static/blog/my-life-story-with-tech/og.png

This is a binary file and will not be displayed.

static/blog/og.png

This is a binary file and will not be displayed.

static/blog/remove-exif-git-hook/og.png

This is a binary file and will not be displayed.

static/blog/spherical-ray-diagrams/og.png

This is a binary file and will not be displayed.

static/blog/spotify-to-apple-music/og.png

This is a binary file and will not be displayed.

static/blog/ssd-removal-mbp-2017/og.png

This is a binary file and will not be displayed.

static/blog/tangled-sync/og.png

This is a binary file and will not be displayed.

static/blog/test-post/og.png

This is a binary file and will not be displayed.

static/click.ogg

This is a binary file and will not be displayed.

static/favicon/apple-touch-icon.png

This is a binary file and will not be displayed.

static/favicon/favicon-96x96.png

This is a binary file and will not be displayed.

static/favicon/favicon.ico

This is a binary file and will not be displayed.

+21
static/favicon/site.webmanifest
···
··· 1 + { 2 + "name": "site@zera", 3 + "short_name": "site@zera", 4 + "icons": [ 5 + { 6 + "src": "/favicon/web-app-manifest-192x192.png", 7 + "sizes": "192x192", 8 + "type": "image/png", 9 + "purpose": "maskable" 10 + }, 11 + { 12 + "src": "/favicon/web-app-manifest-512x512.png", 13 + "sizes": "512x512", 14 + "type": "image/png", 15 + "purpose": "maskable" 16 + } 17 + ], 18 + "theme_color": "#272631", 19 + "background_color": "#272631", 20 + "display": "standalone" 21 + }
static/favicon/web-app-manifest-192x192.png

This is a binary file and will not be displayed.

static/favicon/web-app-manifest-512x512.png

This is a binary file and will not be displayed.

static/favicon-16x16.png

This is a binary file and will not be displayed.

static/favicon-32x32.png

This is a binary file and will not be displayed.

static/favicon.ico

This is a binary file and will not be displayed.

-21
static/icons.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg"> 2 - <symbol id="rss" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" /> 3 - <path fill="currentColor" d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1Z"/> 4 - </symbol> 5 - 6 - <symbol id="darkMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" /> 7 - <path fill="currentColor" d="M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z"/> 8 - </symbol> 9 - 10 - <symbol id="lightMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" /> 11 - <path fill="currentColor" d="M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z"/> 12 - </symbol> 13 - 14 - <symbol id="chevronLeft" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" /> 15 - <path fill="currentColor" d="M15.41 16.58L10.83 12l4.58-4.59L14 6l-6 6l6 6l1.41-1.42Z"/> 16 - </symbol> 17 - 18 - <symbol id="chevronRight" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" /> 19 - <path fill="currentColor" d="M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.42Z"/> 20 - </symbol> 21 - </svg>
···
+149
static/js/404-matcher.js
···
··· 1 + // Taken from Vale's 404 Guesser 2 + // https://vale.rocks/assets/scripts/404-guesser.js 3 + // which was based on Gwern's 404 Error Page URL Suggester 4 + // https://gwern.net/static/js/404-guesser.js 5 + 6 + class URLSuggester { 7 + constructor() { 8 + this.maxDistance = 8; 9 + this.urls = []; 10 + } 11 + 12 + async initialize() { 13 + try { 14 + const sitemapText = await this.fetchSitemap(); 15 + if (sitemapText) { 16 + this.urls = this.parseUrls(sitemapText); 17 + const currentPath = window.location.pathname; 18 + if (!currentPath.endsWith("/404")) { 19 + const suggestions = this.findSimilarUrls(currentPath); 20 + this.injectSuggestions(currentPath, suggestions); 21 + } 22 + } 23 + } catch (error) { 24 + console.error("Error initializing URL suggester:", error); 25 + } 26 + } 27 + 28 + async fetchSitemap() { 29 + try { 30 + const response = await fetch("/sitemap.xml"); 31 + return await response.text(); 32 + } catch (error) { 33 + console.error("Error fetching sitemap:", error); 34 + return null; 35 + } 36 + } 37 + 38 + parseUrls(sitemapText) { 39 + const parser = new DOMParser(); 40 + const xmlDoc = parser.parseFromString(sitemapText, "text/xml"); 41 + const urlNodes = xmlDoc.getElementsByTagName("url"); 42 + return Array.from(urlNodes).map( 43 + (node) => 44 + new URL(node.getElementsByTagName("loc")[0].textContent).pathname, 45 + ); 46 + } 47 + 48 + boundedLevenshteinDistance(a, b, maxDistance) { 49 + if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1; 50 + const matrix = Array(b.length + 1) 51 + .fill(null) 52 + .map((_, i) => [i]); 53 + for (let j = 1; j <= a.length; j++) { 54 + matrix[0][j] = j; 55 + } 56 + for (let i = 1; i <= b.length; i++) { 57 + let minDistance = maxDistance + 1; 58 + for (let j = 1; j <= a.length; j++) { 59 + if (b.charAt(i - 1) === a.charAt(j - 1)) { 60 + matrix[i][j] = matrix[i - 1][j - 1]; 61 + } else { 62 + matrix[i][j] = Math.min( 63 + matrix[i - 1][j - 1] + 1, 64 + matrix[i][j - 1] + 1, 65 + matrix[i - 1][j] + 1, 66 + ); 67 + } 68 + minDistance = Math.min(minDistance, matrix[i][j]); 69 + } 70 + if (minDistance > maxDistance) { 71 + return maxDistance + 1; 72 + } 73 + } 74 + return matrix[b.length][a.length]; 75 + } 76 + 77 + findSimilarUrls(targetUrl) { 78 + const targetPath = new URL(targetUrl, location.origin).pathname; 79 + 80 + if (targetPath.startsWith("/posts/")) { 81 + const exactMatch = this.urls.find((url) => url === targetPath); 82 + if (exactMatch) { 83 + return [location.origin + exactMatch]; 84 + } 85 + } 86 + 87 + const potentialMatches = this.urls.filter( 88 + (url) => 89 + Math.abs(url.length - targetPath.length) <= this.maxDistance && 90 + !url.endsWith("/404.html"), 91 + ); 92 + 93 + const similarUrls = potentialMatches 94 + .map((url) => ({ 95 + url, 96 + distance: this.boundedLevenshteinDistance( 97 + url, 98 + targetPath, 99 + this.maxDistance, 100 + ), 101 + })) 102 + .filter((item) => item.distance <= this.maxDistance) 103 + .sort((a, b) => a.distance - b.distance); 104 + 105 + const seenUrls = new Set(); 106 + const uniqueSimilarUrls = similarUrls 107 + .filter((item) => { 108 + if (seenUrls.has(item.url)) return false; 109 + seenUrls.add(item.url); 110 + return true; 111 + }) 112 + .slice(0, 10); 113 + 114 + return uniqueSimilarUrls.map((item) => location.origin + item.url); 115 + } 116 + 117 + injectSuggestions(currentPath, suggestions) { 118 + const app = document.querySelector("#suggestions"); 119 + if (!app) return; 120 + 121 + if (suggestions.length > 0) { 122 + const p = document.createElement("p"); 123 + 124 + p.innerHTML = "I did however find some URLs that might be relevant?"; 125 + app.appendChild(p); 126 + 127 + for (const url of suggestions) { 128 + const a = document.createElement("a"); 129 + const cleanUrl = url.replace(/\.html$/, ""); 130 + a.href = cleanUrl; 131 + a.textContent = cleanUrl; 132 + app.appendChild(a); 133 + } 134 + 135 + const endText = document.createElement("p"); 136 + app.appendChild(endText); 137 + } else { 138 + const p = document.createElement("p"); 139 + p.innerHTML = `Couldn't find any URLs similar to <code>${currentPath}</code>. I guess it's time to find something new`; 140 + app.appendChild(p); 141 + } 142 + 143 + app.className = "url-suggestions"; 144 + } 145 + } 146 + 147 + document.addEventListener("DOMContentLoaded", () => { 148 + new URLSuggester().initialize(); 149 + });
+82
static/js/copy-button.js
···
··· 1 + // Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html 2 + 3 + function initCopyButtons() { 4 + const blocks = document.querySelectorAll("pre[class^='language-']"); 5 + 6 + for (const block of blocks) { 7 + // Code block header title 8 + const title = document.createElement("span"); 9 + title.style.color = "var(--accent-text)"; 10 + const lang = block.getAttribute("data-lang"); 11 + const comment = 12 + block.previousElementSibling && 13 + (block.previousElementSibling.tagName === "blockquote" || 14 + block.previousElementSibling.nodeName === "BLOCKQUOTE") 15 + ? block.previousElementSibling 16 + : null; 17 + if (comment) block.previousElementSibling.remove(); 18 + title.innerHTML = 19 + lang + (comment ? ` (${comment.textContent.trim()})` : ""); 20 + 21 + // Copy button icon 22 + const icon = document.createElement("i"); 23 + icon.classList.add("icon"); 24 + 25 + // Copy button 26 + const button = document.createElement("button"); 27 + const copyCodeText = "Copy code"; 28 + button.setAttribute("title", copyCodeText); 29 + button.appendChild(icon); 30 + 31 + // Code block header 32 + const header = document.createElement("div"); 33 + header.classList.add("header"); 34 + header.appendChild(title); 35 + header.appendChild(button); 36 + 37 + // Container that holds header and the code block itself 38 + const container = document.createElement("div"); 39 + container.classList.add("pre-container"); 40 + container.appendChild(header); 41 + 42 + // Move code block into the container 43 + block.parentNode.insertBefore(container, block); 44 + container.appendChild(block); 45 + 46 + button.addEventListener("click", async () => { 47 + await copyCode(block, header, button); 48 + }); 49 + } 50 + 51 + async function copyCode(block, header, button) { 52 + const code = block.querySelector("code"); 53 + const text = code.innerText; 54 + 55 + // Only try to copy if clipboard API is available 56 + if (navigator.clipboard) { 57 + try { 58 + await navigator.clipboard.writeText(text); 59 + header.classList.add("active"); 60 + button.setAttribute("disabled", true); 61 + 62 + header.addEventListener( 63 + "animationend", 64 + () => { 65 + header.classList.remove("active"); 66 + button.removeAttribute("disabled"); 67 + }, 68 + { once: true }, 69 + ); 70 + } catch (err) { 71 + console.error("Failed to copy:", err); 72 + } 73 + } 74 + } 75 + } 76 + 77 + // Since the script has defer attribute, the DOM is already loaded when this runs 78 + if (document.readyState === 'loading') { 79 + document.addEventListener('DOMContentLoaded', initCopyButtons); 80 + } else { 81 + initCopyButtons(); 82 + }
+65
static/js/emoji-replace.js
···
··· 1 + document.addEventListener("DOMContentLoaded", () => { 2 + const content = document.querySelector("main"); 3 + if (!content) return; 4 + 5 + const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, { 6 + acceptNode: (node) => { 7 + // Skip code blocks, pre tags, and script/style tags 8 + let parent = node.parentElement; 9 + while (parent) { 10 + const tag = parent.tagName.toLowerCase(); 11 + if ( 12 + tag === "code" || 13 + tag === "pre" || 14 + tag === "script" || 15 + tag === "style" 16 + ) { 17 + return NodeFilter.FILTER_REJECT; 18 + } 19 + parent = parent.parentElement; 20 + } 21 + return NodeFilter.FILTER_ACCEPT; 22 + }, 23 + }); 24 + 25 + const nodesToReplace = []; 26 + while (walker.nextNode()) { 27 + const node = walker.currentNode; 28 + if (/:[\w-]+:/.test(node.textContent)) { 29 + nodesToReplace.push(node); 30 + } 31 + } 32 + 33 + nodesToReplace.forEach((node) => { 34 + const frag = document.createDocumentFragment(); 35 + const parts = node.textContent.split(/(:[\w-]+:)/); 36 + 37 + parts.forEach((part) => { 38 + if (/^:[\w-]+:$/.test(part)) { 39 + const name = part.slice(1, -1); 40 + 41 + const span = document.createElement("span"); 42 + span.className = "emoji-inline--wrapper"; 43 + 44 + const img = document.createElement("img"); 45 + img.src = `https://cachet.dunkirk.sh/emojis/${name}/r`; 46 + img.alt = part; 47 + img.className = "emoji-inline"; 48 + img.loading = "lazy"; 49 + img.setAttribute("aria-label", `${name} emoji`); 50 + 51 + // Fallback: if image fails to load, show original text 52 + img.onerror = () => { 53 + span.replaceWith(document.createTextNode(part)); 54 + }; 55 + 56 + span.appendChild(img); 57 + frag.appendChild(span); 58 + } else if (part) { 59 + frag.appendChild(document.createTextNode(part)); 60 + } 61 + }); 62 + 63 + node.replaceWith(frag); 64 + }); 65 + });
+92
static/js/lightbox.js
···
··· 1 + let currentLightboxImages = []; 2 + let currentLightboxIndex = 0; 3 + 4 + function openLightbox(src) { 5 + currentLightboxImages = [src]; 6 + currentLightboxIndex = 0; 7 + showLightbox(); 8 + } 9 + 10 + function openLightboxGroup(element) { 11 + const group = element.closest('.img-group'); 12 + const images = Array.from(group.querySelectorAll('img')).map(img => img.src); 13 + const clickedImg = element.querySelector('img'); 14 + 15 + currentLightboxImages = images; 16 + currentLightboxIndex = images.indexOf(clickedImg.src); 17 + showLightbox(); 18 + } 19 + 20 + function showLightbox() { 21 + let lightbox = document.getElementById('lightbox'); 22 + 23 + if (!lightbox) { 24 + lightbox = document.createElement('div'); 25 + lightbox.id = 'lightbox'; 26 + lightbox.innerHTML = ` 27 + <div class="lightbox-content"> 28 + <button class="lightbox-close" onclick="closeLightbox()">&times;</button> 29 + <img id="lightbox-img" src="" alt=""> 30 + <div class="lightbox-controls"> 31 + <button class="lightbox-prev" onclick="prevImage()">โ†</button> 32 + <button class="lightbox-next" onclick="nextImage()">โ†’</button> 33 + </div> 34 + </div> 35 + `; 36 + document.body.appendChild(lightbox); 37 + 38 + lightbox.addEventListener('click', (e) => { 39 + if (e.target === lightbox) closeLightbox(); 40 + }); 41 + 42 + document.addEventListener('keydown', handleKeyPress); 43 + } 44 + 45 + updateLightboxImage(); 46 + lightbox.style.display = 'flex'; 47 + document.body.style.overflow = 'hidden'; 48 + } 49 + 50 + function closeLightbox() { 51 + const lightbox = document.getElementById('lightbox'); 52 + if (lightbox) { 53 + lightbox.style.display = 'none'; 54 + document.body.style.overflow = ''; 55 + } 56 + } 57 + 58 + function updateLightboxImage() { 59 + const img = document.getElementById('lightbox-img'); 60 + const controls = document.querySelector('.lightbox-controls'); 61 + 62 + img.src = currentLightboxImages[currentLightboxIndex]; 63 + 64 + if (currentLightboxImages.length === 1) { 65 + controls.style.display = 'none'; 66 + } else { 67 + controls.style.display = 'flex'; 68 + } 69 + } 70 + 71 + function prevImage() { 72 + currentLightboxIndex = (currentLightboxIndex - 1 + currentLightboxImages.length) % currentLightboxImages.length; 73 + updateLightboxImage(); 74 + } 75 + 76 + function nextImage() { 77 + currentLightboxIndex = (currentLightboxIndex + 1) % currentLightboxImages.length; 78 + updateLightboxImage(); 79 + } 80 + 81 + function handleKeyPress(e) { 82 + const lightbox = document.getElementById('lightbox'); 83 + if (!lightbox || lightbox.style.display !== 'flex') return; 84 + 85 + if (e.key === 'Escape') { 86 + closeLightbox(); 87 + } else if (e.key === 'ArrowLeft') { 88 + prevImage(); 89 + } else if (e.key === 'ArrowRight') { 90 + nextImage(); 91 + } 92 + }
+142
static/js/relative-time.js
···
··· 1 + class RelativeTimeElement extends HTMLElement { 2 + static get observedAttributes() { 3 + return ['datetime', 'threshold', 'prefix', 'format']; 4 + } 5 + 6 + connectedCallback() { 7 + this.update(); 8 + } 9 + 10 + disconnectedCallback() { 11 + this.stopTimer(); 12 + } 13 + 14 + attributeChangedCallback() { 15 + this.update(); 16 + } 17 + 18 + scheduleUpdate(ms) { 19 + this.stopTimer(); 20 + this.timer = setTimeout(() => this.update(), ms); 21 + } 22 + 23 + stopTimer() { 24 + if (this.timer) { 25 + clearTimeout(this.timer); 26 + this.timer = null; 27 + } 28 + } 29 + 30 + get datetime() { 31 + return this.getAttribute('datetime') || ''; 32 + } 33 + 34 + get threshold() { 35 + return this.getAttribute('threshold') || 'P30D'; 36 + } 37 + 38 + get prefix() { 39 + return this.getAttribute('prefix') || 'on'; 40 + } 41 + 42 + get format() { 43 + return this.getAttribute('format') || 'relative'; 44 + } 45 + 46 + parseThreshold(iso) { 47 + const match = iso.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/); 48 + if (!match) return 30 * 24 * 60 * 60 * 1000; 49 + const days = parseInt(match[1] || 0, 10); 50 + const hours = parseInt(match[2] || 0, 10); 51 + const minutes = parseInt(match[3] || 0, 10); 52 + const seconds = parseInt(match[4] || 0, 10); 53 + return ((days * 24 + hours) * 60 + minutes) * 60 * 1000 + seconds * 1000; 54 + } 55 + 56 + update() { 57 + const datetime = this.datetime; 58 + if (!datetime) return; 59 + 60 + const date = new Date(datetime); 61 + if (isNaN(date.getTime())) return; 62 + 63 + const now = Date.now(); 64 + const diff = now - date.getTime(); 65 + const absDiff = Math.abs(diff); 66 + const thresholdMs = this.parseThreshold(this.threshold); 67 + 68 + if (this.format === 'datetime' || absDiff > thresholdMs) { 69 + this.textContent = this.formatDatetime(date); 70 + this.scheduleUpdate(3600000); 71 + } else { 72 + this.textContent = this.formatRelative(diff); 73 + this.scheduleUpdate(this.getNextUpdateDelay(absDiff)); 74 + } 75 + } 76 + 77 + getNextUpdateDelay(absDiff) { 78 + const seconds = Math.floor(absDiff / 1000); 79 + const minutes = Math.floor(seconds / 60); 80 + const hours = Math.floor(minutes / 60); 81 + const days = Math.floor(hours / 24); 82 + 83 + if (seconds < 60) { 84 + return 1000; 85 + } else if (minutes < 60) { 86 + return 60000; 87 + } else if (hours < 24) { 88 + return 60000 * 5; 89 + } else if (days < 7) { 90 + return 3600000; 91 + } else { 92 + return 3600000 * 6; 93 + } 94 + } 95 + 96 + formatRelative(diff) { 97 + const rtf = new Intl.RelativeTimeFormat(navigator.language, { 98 + numeric: 'auto', 99 + style: 'long' 100 + }); 101 + 102 + const absDiff = Math.abs(diff); 103 + const sign = diff > 0 ? -1 : 1; 104 + const seconds = Math.floor(absDiff / 1000); 105 + const minutes = Math.floor(seconds / 60); 106 + const hours = Math.floor(minutes / 60); 107 + const days = Math.floor(hours / 24); 108 + const months = Math.floor(days / 30); 109 + const years = Math.floor(days / 365); 110 + 111 + if (seconds < 60) { 112 + return rtf.format(sign * seconds, 'second'); 113 + } else if (minutes < 60) { 114 + return rtf.format(sign * minutes, 'minute'); 115 + } else if (hours < 24) { 116 + return rtf.format(sign * hours, 'hour'); 117 + } else if (days < 30) { 118 + return rtf.format(sign * days, 'day'); 119 + } else if (months < 12) { 120 + return rtf.format(sign * months, 'month'); 121 + } else { 122 + return rtf.format(sign * years, 'year'); 123 + } 124 + } 125 + 126 + formatDatetime(date) { 127 + const now = new Date(); 128 + const sameYear = date.getFullYear() === now.getFullYear(); 129 + 130 + const options = { 131 + month: 'short', 132 + day: 'numeric', 133 + ...(sameYear ? {} : { year: 'numeric' }) 134 + }; 135 + 136 + const prefix = this.prefix; 137 + const formatted = new Intl.DateTimeFormat(navigator.language, options).format(date); 138 + return prefix ? `${prefix} ${formatted}` : formatted; 139 + } 140 + } 141 + 142 + customElements.define('relative-time', RelativeTimeElement);
-54
static/js/script.js
··· 1 - const toggleButton = document.getElementById("theme-toggle"); 2 - const themeIcon = document.getElementById("theme-icon"); 3 - const themeSound = document.getElementById("theme-sound"); 4 - 5 - // Function to update the theme icon based on the current theme 6 - const updateThemeIcon = (isDarkMode) => { 7 - const themeMode = isDarkMode ? "darkMode" : "lightMode"; 8 - const iconPath = themeIcon 9 - .querySelector("use") 10 - .getAttribute("href") 11 - .replace(/#.*$/, `#${themeMode}`); 12 - themeIcon.querySelector("use").setAttribute("href", iconPath); 13 - }; 14 - 15 - // Function to update the theme based on the current mode 16 - const updateTheme = (isDarkMode) => { 17 - const theme = isDarkMode ? "dark" : "light"; 18 - document.documentElement.setAttribute("data-theme", theme); 19 - updateThemeIcon(isDarkMode); 20 - }; 21 - 22 - // Function to toggle the theme 23 - const toggleTheme = () => { 24 - const isDarkMode = toggleButton.checked; 25 - updateTheme(isDarkMode); 26 - themeSound.play(); 27 - localStorage.setItem("theme", isDarkMode ? "dark" : "light"); 28 - 29 - // Add transition class to body for smooth transition 30 - document.body.classList.add("theme-transition"); 31 - setTimeout(() => { 32 - document.body.classList.remove("theme-transition"); 33 - }, 300); 34 - }; 35 - 36 - // Event listener for theme toggle 37 - toggleButton.addEventListener("change", toggleTheme); 38 - 39 - // Function to initialize the theme based on the stored preference 40 - const initializeTheme = () => { 41 - const storedTheme = localStorage.getItem("theme"); 42 - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; 43 - const isDarkMode = storedTheme === "dark" || (!storedTheme && prefersDark); 44 - toggleButton.checked = isDarkMode; 45 - updateTheme(isDarkMode); 46 - }; 47 - 48 - // Initialize the theme 49 - initializeTheme(); 50 - 51 - // Listen for changes in system preference 52 - window 53 - .matchMedia("(prefers-color-scheme: dark)") 54 - .addEventListener("change", initializeTheme);
···
static/og.png

This is a binary file and will not be displayed.

static/pfp/og.png

This is a binary file and will not be displayed.

static/pfps/current.webp

This is a binary file and will not be displayed.

static/pfps/fall.jpg

This is a binary file and will not be displayed.

static/pfps/hands.jpg

This is a binary file and will not be displayed.

static/pfps/instsqc-rat-pfp.webp

This is a binary file and will not be displayed.

static/pfps/kieranrat.webp

This is a binary file and will not be displayed.

static/pfps/kitty.webp

This is a binary file and will not be displayed.

static/pfps/starry.webp

This is a binary file and will not be displayed.

-19
static/site.webmanifest
··· 1 - { 2 - "name": "", 3 - "short_name": "", 4 - "icons": [ 5 - { 6 - "src": "/android-chrome-192x192.png", 7 - "sizes": "192x192", 8 - "type": "image/png" 9 - }, 10 - { 11 - "src": "/android-chrome-512x512.png", 12 - "sizes": "512x512", 13 - "type": "image/png" 14 - } 15 - ], 16 - "theme_color": "#ffffff", 17 - "background_color": "#ffffff", 18 - "display": "standalone" 19 - }
···
static/tags/accessibility/og.png

This is a binary file and will not be displayed.

static/tags/apple/og.png

This is a binary file and will not be displayed.

static/tags/archival/og.png

This is a binary file and will not be displayed.

static/tags/atproto/og.png

This is a binary file and will not be displayed.

static/tags/biography/og.png

This is a binary file and will not be displayed.

static/tags/cool-stuff/og.png

This is a binary file and will not be displayed.

static/tags/essays/og.png

This is a binary file and will not be displayed.

static/tags/fancy/og.png

This is a binary file and will not be displayed.

static/tags/graphql/og.png

This is a binary file and will not be displayed.

static/tags/hilton/og.png

This is a binary file and will not be displayed.

static/tags/homelab/og.png

This is a binary file and will not be displayed.

static/tags/meta/og.png

This is a binary file and will not be displayed.

static/tags/mildrant/og.png

This is a binary file and will not be displayed.

static/tags/music/og.png

This is a binary file and will not be displayed.

static/tags/nix/og.png

This is a binary file and will not be displayed.

static/tags/og.png

This is a binary file and will not be displayed.

static/tags/physics/og.png

This is a binary file and will not be displayed.

static/tags/project/og.png

This is a binary file and will not be displayed.

static/tags/reverse-engineering/og.png

This is a binary file and will not be displayed.

static/tags/shell/og.png

This is a binary file and will not be displayed.

static/tags/teardown/og.png

This is a binary file and will not be displayed.

static/tags/tool/og.png

This is a binary file and will not be displayed.

static/tags/tutorial/og.png

This is a binary file and will not be displayed.

static/tags/yap-fest/og.png

This is a binary file and will not be displayed.

static/verify/og.png

This is a binary file and will not be displayed.

+433
syntaxes/authorized-keys.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # https://www.sublimetext.com/docs/syntax.html 4 + # https://man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT 5 + # https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT 6 + name: Authorized Keys 7 + scope: text.authorized_keys 8 + version: 2 9 + extends: SSH Crypto.sublime-syntax 10 + 11 + file_extensions: 12 + - authorized_keys 13 + - pub 14 + 15 + hidden_file_extensions: 16 + - authorized_keys2 17 + 18 + contexts: 19 + main: 20 + - include: comments-number-sign 21 + - match: ^ 22 + push: 23 + - meta_scope: meta.line.authorized-key.authorized_keys 24 + - include: pop-before-nl 25 + - include: pop-nl 26 + - include: ssh-key-types 27 + - include: ssh-fingerprint-with-label 28 + - include: flag-options 29 + - include: value-options 30 + - include: strings 31 + - match: = 32 + scope: keyword.operator.assignment.authorized_keys 33 + - include: punctuation-comma-sequence 34 + 35 + flag-options: 36 + - match: (?:no-)?(?:pty|user-rc|(?:agent|port|X11)-forwarding) 37 + scope: keyword.other.authorized_keys 38 + - match: (?:no-touch-required|verify-required|cert-authority|restrict) 39 + scope: keyword.other.authorized_keys 40 + 41 + value-options: 42 + - match: (principals)(=) 43 + captures: 44 + 1: keyword.other.authorized_keys 45 + 2: keyword.operator.assignment.authorized_keys 46 + with_prototype: 47 + - include: punctuation-comma-sequence 48 + push: value-option-body 49 + 50 + - match: (tunnel)(=) 51 + captures: 52 + 1: keyword.other.authorized_keys 53 + 2: keyword.operator.assignment.authorized_keys 54 + with_prototype: 55 + - match: \d{1,3} 56 + scope: meta.number.integer.decimal.authorized_keys 57 + constant.numeric.value.authorized_keys 58 + push: value-option-body 59 + 60 + - match: (?:(expiry-time)|(valid-before))(=) 61 + captures: 62 + 1: keyword.other.authorized_keys 63 + 2: invalid.deprecated.authorized_keys 64 + 3: keyword.operator.assignment.authorized_keys 65 + with_prototype: 66 + - match: |- 67 + (?x: 68 + \d{4} # Year 69 + (?:0\d|1[12]) # Month 70 + (?:[0-2]\d|3[01]) # Day 71 + (?: # Optionally: 72 + (?:[01]\d|2[0-3]) # HH 73 + (?:[0-5]\d){1,2} # MM and maybe SS 74 + )? 75 + Z? # Optional UTC 76 + ) 77 + scope: meta.constant.date.authorized_keys 78 + constant.numeric.integer.date.authorized_keys 79 + push: value-option-body 80 + 81 + # Technically, permitopen requires a host, but let's be lenient 82 + - match: (permitlisten|permitopen)(=) 83 + captures: 84 + 1: keyword.other.authorized_keys 85 + 2: keyword.operator.assignment.authorized_keys 86 + with_prototype: 87 + - include: ipv4 88 + - include: ipv6-square-bracket 89 + - match: (?:([^"]*)(:))?(?:({{zero_to_65535}})|(\*)) 90 + captures: 91 + 1: meta.string.host.authorized_keys 92 + 2: punctuation.separator.sequence.authorized_keys 93 + 3: meta.number.integer.decimal.authorized_keys 94 + constant.numeric.port-number.authorized_keys 95 + 4: constant.other.wildcard.asterisk.authorized_keys 96 + push: value-option-body 97 + 98 + - match: (from)(=) 99 + captures: 100 + 1: keyword.other.authorized_keys 101 + 2: keyword.operator.assignment.authorized_keys 102 + with_prototype: 103 + - include: operator-exclamation 104 + - include: punctuation-comma-sequence 105 + - include: punctuation-dot-sequence 106 + - include: wildcards 107 + push: value-option-body 108 + 109 + - match: (environment)(=) 110 + captures: 111 + 1: keyword.other.authorized_keys 112 + 2: keyword.operator.assignment.authorized_keys 113 + with_prototype: 114 + - match: (\w+)(=) 115 + captures: 116 + 1: variable.other.readwrite.authorized_keys 117 + 2: keyword.operator.assignment.authorized_keys 118 + push: value-option-body 119 + 120 + - match: (command)(=)(") 121 + captures: 122 + 1: keyword.other.authorized_keys 123 + 2: keyword.operator.assignment.authorized_keys 124 + 3: string.quoted.double.authorized_keys 125 + punctuation.definition.string.begin.authorized_keys 126 + # TODO: Allow escaped double-quote 127 + embed: scope:source.shell.bash 128 + embed_scope: source.shell.embedded 129 + escape: '"|(?=$)' 130 + escape_captures: 131 + 0: string.quoted.double.authorized_keys 132 + punctuation.definition.string.end.authorized_keys 133 + 134 + value-option-body: 135 + - include: strings 136 + - match: (?=,|\s) 137 + pop: 1 138 + - match: . 139 + scope: invalid.illegal.authorized_keys 140 + pop: 1 141 + 142 + strings: 143 + - match: '"' 144 + scope: punctuation.definition.string.begin.authorized_keys 145 + push: 146 + - meta_scope: string.quoted.double.authorized_keys 147 + - match: \\" 148 + scope: constant.character.escape.authorized_keys 149 + - match: '"' 150 + scope: punctuation.definition.string.end.authorized_keys 151 + pop: 1 152 + 153 + 154 + 155 + comments: 156 + - include: comments-number-sign 157 + - include: comments-semicolon 158 + 159 + comments-number-sign: 160 + - match: ^\s*(#+) 161 + captures: 162 + 1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common 163 + push: 164 + - meta_content_scope: comment.line.number-sign.ssh.common 165 + - match: \n 166 + scope: comment.line.number-sign.ssh.common 167 + pop: true 168 + 169 + comments-semicolon: 170 + - match: ^\s*(;+) 171 + captures: 172 + 1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common 173 + push: 174 + - meta_content_scope: comment.line.semi-colon.ssh.common 175 + - include: pop-nl 176 + 177 + ###[ COMPONENTS ]############################################################## 178 + 179 + operator-exclamation: 180 + - match: '!' 181 + scope: keyword.operator.logical.ssh.common 182 + 183 + wildcards: 184 + - match: \* 185 + scope: constant.other.wildcard.asterisk.ssh.common 186 + - match: \? 187 + scope: constant.other.wildcard.questionmark.ssh.common 188 + 189 + punctuation-comma-sequence: 190 + - match: ',' 191 + scope: punctuation.separator.sequence.ssh.common 192 + 193 + punctuation-dot-sequence: 194 + - match: \. 195 + scope: punctuation.separator.sequence.ssh.common 196 + 197 + punctuation-at: 198 + - match: '@' 199 + scope: punctuation.separator.sequence.ssh.common 200 + 201 + ssh-fingerprint: 202 + - match: '{{ssh_fingerprint}}' 203 + scope: variable.other.fingerprint.ssh.common 204 + 205 + ssh-fingerprint-with-label: 206 + - match: '{{ssh_fingerprint}}' 207 + scope: variable.other.fingerprint.ssh.common 208 + push: expect-fingerprint-label 209 + 210 + expect-fingerprint-label: 211 + - include: pop-before-nl 212 + - match: (?=\S) 213 + push: 214 + - meta_scope: meta.annotation.identifier.ssh.common 215 + string.unquoted.ssh.common 216 + - match: '(?=[ \t]*$)' 217 + pop: 1 218 + - include: punctuation-at 219 + 220 + time-values: 221 + # https://man.openbsd.org/sshd_config.5#TIME_FORMATS 222 + # seconds, minutes, hours, days, weeks 223 + - match: \b(?=[\dsmhdw]*\d[smhdw][\s,"]) 224 + push: 225 + - meta_scope: meta.constant.time.ssh.common 226 + meta.number.integer.decimal.ssh.common 227 + - match: (?=[\s,"]) 228 + pop: 1 229 + - match: (\d+)([smhdw]) 230 + captures: 231 + 1: constant.numeric.value.ssh.common 232 + 2: constant.numeric.suffix.ssh.common 233 + 234 + bytes-values: 235 + - match: \b(\d+)([KMG])(?=[\s,"]) 236 + scope: meta.constant.bytes.ssh.common 237 + meta.number.integer.other.ssh.common 238 + captures: 239 + 1: constant.numeric.value.ssh.common 240 + 2: constant.numeric.suffix.ssh.common 241 + 242 + mac-addresses: 243 + - match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2}) 244 + scope: entity.name.constant.mac-address.ssh.common 245 + 246 + ipv4: 247 + - match: '\b{{ipv4}}\b' 248 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 249 + 250 + ipv6: 251 + - match: '{{ipv6}}' 252 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 253 + 254 + ipv6-square-bracket: 255 + - match: (\[){{ipv6}}(\]) 256 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 257 + captures: 258 + 1: punctuation.definition.constant.begin.ssh.common 259 + 2: punctuation.definition.constant.end.ssh.common 260 + 261 + ip-addresses: 262 + - include: ipv6 263 + - include: ipv4 264 + 265 + ipv4-with-cidr: 266 + - match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b 267 + captures: 268 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 269 + 2: punctuation.separator.sequence.ssh.common 270 + 3: constant.other.range.ssh.common 271 + 272 + ipv6-with-cidr: 273 + - match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)? 274 + captures: 275 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 276 + 2: punctuation.separator.sequence.ssh.common 277 + 3: constant.other.range.ssh.common 278 + 279 + ip-addresses-with-cidr: 280 + - include: ipv6-with-cidr 281 + - include: ipv4-with-cidr 282 + 283 + port-numbers: 284 + - match: \b{{zero_to_65535}}(?![\w:]) 285 + scope: meta.number.integer.decimal.ssh.common 286 + constant.numeric.port-number.ssh.common 287 + 288 + match-all: 289 + - match: '\b(?xi: all )\b' 290 + scope: constant.language.boolean.true.ssh.common 291 + 292 + none: 293 + - match: \bnone\b 294 + scope: constant.language.null.ssh.common 295 + 296 + any: 297 + - match: \bany\b 298 + scope: constant.language.set.ssh.common 299 + 300 + boolean: 301 + - match: \byes\b 302 + scope: constant.language.boolean.true.ssh.common 303 + - match: \bno\b 304 + scope: constant.language.boolean.false.ssh.common 305 + 306 + boolean-with-typing: 307 + - include: boolean 308 + # Consume while typing as well, but unscoped 309 + - match: \b(?:ye?|n)\b 310 + 311 + log-level: 312 + - match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b' 313 + scope: constant.language.log-level.ssh.common 314 + 315 + possibly-quoted-value: 316 + - meta_content_scope: meta.mapping.value.ssh.common 317 + - match: '"' 318 + scope: punctuation.definition.string.begin.ssh.common 319 + push: 320 + - meta_scope: string.quoted.double.ssh.common 321 + - match: (")(?:\s*(\S.*))? 322 + captures: 323 + 1: punctuation.definition.string.end.ssh.common 324 + 2: invalid.illegal.ssh.common 325 + pop: 1 326 + - match: \n|$ 327 + scope: invalid.illegal.unclosed-string.ssh.common 328 + pop: 2 329 + - match: (?=\S) 330 + push: 331 + - meta_content_scope: string.unquoted.ssh.common 332 + - include: pop-before-nl 333 + - include: pop-nl 334 + 335 + string-patterns: 336 + # https://man7.org/linux/man-pages/man5/ssh_config.5.html#PATTERNS 337 + # https://man.openbsd.org/ssh_config.5#PATTERNS 338 + # https://man7.org/linux/man-pages/man5/sshd_config.5.html#PATTERNS 339 + # https://man.openbsd.org/sshd_config.5#PATTERNS 340 + - include: punctuation-comma-sequence 341 + - include: operator-exclamation 342 + - match: '"' 343 + scope: punctuation.definition.string.begin.ssh.common 344 + push: 345 + - meta_content_scope: string.quoted.double.ssh.common 346 + - match: '"' 347 + scope: punctuation.definition.string.end.ssh.common 348 + pop: 1 349 + - include: wildcards 350 + - match: (?=\S) 351 + push: 352 + - meta_content_scope: string.unquoted.ssh.common 353 + - match: (?=[,!\s]) 354 + pop: 1 355 + - include: wildcards 356 + 357 + paths: 358 + # This is just heuristic. Expect failures. 359 + - match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?) 360 + push: 361 + - meta_scope: meta.path.ssh.common 362 + entity.name.ssh.common 363 + - match: (?=[\s,"]) 364 + pop: 1 365 + - match: ~[\w\-.]* 366 + scope: variable.language.home.ssh.common 367 + - match: (/)(?:(\.{1,2})(?=/)|\.(?!/))? 368 + captures: 369 + 1: punctuation.separator.path.ssh.common 370 + 2: constant.other.placeholder.ssh.common 371 + - match: \.(?=[\w*?%]) 372 + scope: punctuation.separator.sequence.ssh.common 373 + - include: wildcards 374 + - include: tokens 375 + - include: environment-variables 376 + 377 + none-command-values: 378 + - match: \s*(none)\b[ \t]*$ 379 + captures: 380 + 1: constant.language.null.ssh.common 381 + - match: \s*((")(none)("))[ \t]*$ 382 + captures: 383 + 1: string.quoted.double.ssh.common 384 + 2: punctuation.definition.string.begin.ssh.common 385 + 3: constant.language.null.ssh.common 386 + 4: punctuation.definition.string.end.ssh.common 387 + 388 + tokens: [] 389 + environment-variables: [] 390 + 391 + ###[ PROTOTYPE ]############################################################### 392 + 393 + pop-nl: 394 + - match: \n 395 + pop: 1 396 + 397 + pop-before-nl: 398 + - match: (?=\n) 399 + pop: 1 400 + 401 + ############################################################################### 402 + 403 + 404 + 405 + ssh-ciphers: 406 + - match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 407 + scope: support.function.cipher.ssh.crypto 408 + - match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"]) 409 + scope: invalid.deprecated.cipher.ssh.crypto 410 + ssh-kex-algorithms: 411 + - match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"]) 412 + scope: support.function.kex-algorithm.ssh.crypto 413 + - match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"]) 414 + scope: invalid.deprecated.kex-algorithm.ssh.crypto 415 + ssh-key-types: 416 + - match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"]) 417 + scope: support.type.key-type.ssh.crypto 418 + - match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"]) 419 + scope: invalid.deprecated.key-type.ssh.crypto 420 + ssh-mac-algorithms: 421 + - match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 422 + scope: support.function.mac-algorithm.ssh.crypto 423 + - match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"]) 424 + scope: invalid.deprecated.mac-algorithm.ssh.crypto 425 + extends: SSH Common.sublime-syntax 426 + hidden: true 427 + hidden_file_extensions: 428 + - syntax_test_crypto 429 + name: SSH Crypto 430 + scope: text.ssh.crypto 431 + version: 2 432 + variables: 433 + zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
+243
syntaxes/gleam.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + version: 2 4 + 5 + file_extensions: 6 + - gleam 7 + 8 + scope: source.gleam 9 + 10 + variables: 11 + lower_ident: '[[:lower:]][[:word:]]*' 12 + upper_ident: '[[:upper:]][[:word:]]*' 13 + 14 + contexts: 15 + main: 16 + - include: base 17 + base: 18 + - include: attribute 19 + - include: bitstring 20 + - include: block 21 + - include: comment 22 + - include: constant_def 23 + - include: function_def 24 + - include: keyword 25 + - include: function_call 26 + - include: record 27 + - include: import 28 + - include: number 29 + - include: operator 30 + - include: punctuation 31 + - include: string 32 + - include: unused_name 33 + - include: type_name 34 + 35 + # Attributes (annotations) 36 + attribute: 37 + - match: ^\s*(@{{lower_ident}})\( 38 + captures: 39 + 1: variable.other.constant.gleam 40 + push: 41 + - include: arguments 42 + - meta_scope: meta.annotation.gleam 43 + - match: ^\s*(@{{lower_ident}}) 44 + scope: meta.annotation.gleam 45 + captures: 46 + 1: variable.other.constant.gleam 47 + 48 + # Arguments (to a function call, record constructor, or attribute) 49 + arguments: 50 + - include: bitstring 51 + - include: block 52 + - include: comment 53 + - include: function_def 54 + - include: function_call 55 + - include: record 56 + - include: number 57 + - include: operator 58 + - include: punctuation 59 + - include: string 60 + - include: unused_name 61 + - include: type_name 62 + - match: '\b{{lower_ident}}:' 63 + scope: constant.other.gleam 64 + - match: \) 65 + pop: true 66 + 67 + # Bitstrings 68 + bitstring: 69 + - match: '<<' 70 + scope: punctuation.definition.generic.begin.gleam 71 + push: 72 + - include: number 73 + - include: string 74 + - match: \b(bytes|int|float|bits|utf8|utf16|utf32|utf8_codepoint|utf16_codepoint|utf32_codepoint|signed|unsigned|big|little|native|unit|size)\b 75 + scope: keyword.other.gleam 76 + - match: '>>' 77 + scope: punctuation.definition.generic.end.gleam 78 + pop: true 79 + 80 + # Blocks 81 + block: 82 + - match: '{' 83 + scope: punctuation.section.block.begin.gleam 84 + push: base 85 + - match: '}' 86 + scope: punctuation.section.block.end.gleam 87 + pop: true 88 + 89 + # Comments 90 + comment: 91 + - match: ///?/? 92 + scope: punctuation.definition.comment.line.gleam 93 + push: 94 + - meta_scope: comment.line.gleam 95 + - match: $ 96 + pop: true 97 + 98 + # Constant definitions 99 + constant_def: 100 + - match: \b(const)\s+({{lower_ident}})\b 101 + captures: 102 + 1: keyword.other.gleam 103 + 2: entity.name.constant.gleam 104 + 105 + # Function calls 106 + function_call: 107 + - match: \b(?:{{lower_ident}}\.)*({{lower_ident}})\( 108 + captures: 109 + 1: variable.function.gleam 110 + push: arguments 111 + 112 + # Function definitions 113 + function_def: 114 + - match: \b(fn)(?:[[:space:]]+({{lower_ident}}))?[[:space:]]*\( 115 + captures: 116 + 1: storage.type.function.gleam 117 + 2: entity.name.function.gleam 118 + push: function_def_args 119 + 120 + # Function arguments 121 + function_def_args: 122 + - include: function_def 123 + - include: punctuation 124 + - include: type_name 125 + - include: unused_name 126 + - match: -> 127 + scope: keyword.operator.gleam 128 + - match: \b(?:({{lower_ident}})[[:space:]]+)?({{lower_ident}}:) 129 + captures: 130 + 1: constant.other.gleam 131 + 2: variable.parameter.gleam 132 + - match: \( 133 + push: function_def_args 134 + - match: \) 135 + pop: true 136 + 137 + # Imports 138 + import: 139 + - match: ^import\b 140 + scope: keyword.control.import.gleam 141 + push: 142 + - match: \bas\b 143 + scope: keyword.control.import.gleam 144 + - match: \b(?:{{lower_ident}}/)*{{lower_ident}}\b 145 + scope: entity.name.namespace.gleam 146 + - match: (\.)({) 147 + captures: 148 + 1: punctuation.accessor.gleam 149 + 2: punctuation.definition.generic.begin.gleam 150 + push: 151 + - include: punctuation 152 + - include: type_name 153 + - match: \bas\b 154 + scope: keyword.control.import.gleam 155 + - match: \btype\b 156 + scope: storage.type.gleam 157 + - match: '}' 158 + scope: punctuation.definition.generic.end.gleam 159 + pop: true 160 + - match: $ 161 + pop: true 162 + 163 + # Keywords 164 + keyword: 165 + - match: \b(as|assert|case|const|echo|if|let|panic|todo|use)\b 166 + scope: keyword.other.gleam 167 + - match: \b(opaque|pub)\b 168 + scope: storage.modifier.gleam 169 + - match: \btype\b 170 + scope: storage.type.gleam 171 + - match: \bfn\b 172 + scope: storage.type.function.gleam 173 + # Reserved for future use 174 + - match: \b(auto|delegate|derive|else|implement|macro|test)\b 175 + scope: invalid.illegal.gleam 176 + 177 + # Numbers 178 + number: 179 + - match: \b0b[01][01_]*\b 180 + scope: constant.numeric.binary.gleam 181 + - match: \b0o[0-7][0-7_]*\b 182 + scope: constant.numeric.octal.gleam 183 + - match: \b[0-9][0-9_]*(\.[0-9_]*(e-?[0-9][0-9_]*)?)?\b 184 + scope: constant.numeric.decimal.gleam 185 + - match: \b0x[[:xdigit:]][[:xdigit:]_]*\b 186 + scope: constant.numeric.hexadecimal.gleam 187 + 188 + # Operators 189 + operator: 190 + - match: <- 191 + scope: keyword.operator.assignment.gleam 192 + - match: (\|>|\.\.|<=\.|>=\.|==\.|!=\.|<\.|>\.|<=|>=|==|!=|<|>|<>) 193 + scope: keyword.operator.gleam 194 + - match: '=' 195 + scope: keyword.operator.assignment.gleam 196 + - match: -> 197 + scope: keyword.operator.gleam 198 + - match: (\+\.|\-\.|/\.|\*\.|%\.|\+|\-|/|\*|%) 199 + scope: keyword.operator.arithmetic.gleam 200 + - match: (&&|\|\|) 201 + scope: keyword.operator.logical.gleam 202 + - match: \| 203 + scope: keyword.operator.gleam 204 + 205 + # Punctuation (separators, accessors) 206 + punctuation: 207 + - match: \. 208 + scope: punctuation.accessor.gleam 209 + - match: ',' 210 + scope: punctuation.separator.gleam 211 + 212 + # Records (constructors with arguments) 213 + record: 214 + - match: \b((?:{{lower_ident}}\.)*{{upper_ident}})\( 215 + captures: 216 + 1: entity.name.type.gleam 217 + push: arguments 218 + 219 + # Strings 220 + string: 221 + - match: '"' 222 + scope: punctuation.definition.string.begin.gleam 223 + push: 224 + - meta_scope: string.quoted.double.gleam 225 + - match: \\[fnrt"\\] 226 + scope: constant.character.escape.gleam 227 + - match: \\u\{[[:xdigit:]]{1,6}\} 228 + scope: constant.character.escape.gleam 229 + - match: \\ 230 + scope: invalid.illegal.gleam 231 + - match: '"' 232 + scope: punctuation.definition.string.end.gleam 233 + pop: true 234 + 235 + # Types and constructors 236 + type_name: 237 + - match: \b(?:{{lower_ident}}\.)*{{upper_ident}}\b 238 + scope: entity.name.type.gleam 239 + 240 + # Unused bindings 241 + unused_name: 242 + - match: \b_{{lower_ident}}\b 243 + scope: comment.line.gleam
+305
syntaxes/known-hosts.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # Standalone version of known-hosts.sublime-syntax 4 + # Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax 5 + 6 + name: Known Hosts 7 + scope: text.known_hosts 8 + version: 2 9 + file_extensions: 10 + - known_hosts 11 + hidden_file_extensions: 12 + - known_hosts.old 13 + variables: 14 + base64_char: '[a-zA-Z0-9+/]' 15 + ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3}) 16 + zero_to_32: (?:3[0-2]|[12][0-9]|[0-9]) 17 + zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9]) 18 + zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9]) 19 + zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9]) 20 + ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}}) 21 + ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\ 22 + \ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\ 23 + \ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \ 24 + \ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \ 25 + \ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\ 26 + \ # fe80::7:8%eth0 fe80::7:8%1 \ 27 + \ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\ 28 + \ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\ 29 + \ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\ 30 + \ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\ 31 + \ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\ 32 + \ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\ 33 + \ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\ 34 + \ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\ 35 + \ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \ 36 + \ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \ 37 + \ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)" 38 + contexts: 39 + main: 40 + - include: comments-number-sign 41 + - match: ^((@)(?:revoked|cert-authority))? 42 + captures: 43 + 1: meta.annotation.known_hosts variable.annotation.known_hosts 44 + 2: punctuation.definition.annotation.known_hosts 45 + push: 46 + - meta_scope: meta.line.known-host.known_hosts 47 + - include: pop-before-nl 48 + - include: pop-nl 49 + - include: punctuation-comma-sequence 50 + - include: ssh-fingerprint-with-label 51 + - include: ssh-key-types 52 + - include: hostname-or-ip-value 53 + comments: 54 + - include: comments-number-sign 55 + - include: comments-semicolon 56 + comments-number-sign: 57 + - match: ^\s*(#+) 58 + captures: 59 + 1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common 60 + push: 61 + - meta_content_scope: comment.line.number-sign.ssh.common 62 + - match: \n 63 + scope: comment.line.number-sign.ssh.common 64 + pop: true 65 + comments-semicolon: 66 + - match: ^\s*(;+) 67 + captures: 68 + 1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common 69 + push: 70 + - meta_content_scope: comment.line.semi-colon.ssh.common 71 + - include: pop-nl 72 + operator-exclamation: 73 + - match: '!' 74 + scope: keyword.operator.logical.ssh.common 75 + wildcards: 76 + - match: \* 77 + scope: constant.other.wildcard.asterisk.ssh.common 78 + - match: \? 79 + scope: constant.other.wildcard.questionmark.ssh.common 80 + punctuation-comma-sequence: 81 + - match: ',' 82 + scope: punctuation.separator.sequence.ssh.common 83 + punctuation-dot-sequence: 84 + - match: \. 85 + scope: punctuation.separator.sequence.ssh.common 86 + punctuation-at: 87 + - match: '@' 88 + scope: punctuation.separator.sequence.ssh.common 89 + ssh-fingerprint: 90 + - match: '{{ssh_fingerprint}}' 91 + scope: variable.other.fingerprint.ssh.common 92 + ssh-fingerprint-with-label: 93 + - match: '{{ssh_fingerprint}}' 94 + scope: variable.other.fingerprint.ssh.common 95 + push: expect-fingerprint-label 96 + expect-fingerprint-label: 97 + - include: pop-before-nl 98 + - match: (?=\S) 99 + push: 100 + - meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common 101 + - match: (?=[ \t]*$) 102 + pop: 1 103 + - include: punctuation-at 104 + time-values: 105 + - match: \b(?=[\dsmhdw]*\d[smhdw][\s,"]) 106 + push: 107 + - meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common 108 + - match: (?=[\s,"]) 109 + pop: 1 110 + - match: (\d+)([smhdw]) 111 + captures: 112 + 1: constant.numeric.value.ssh.common 113 + 2: constant.numeric.suffix.ssh.common 114 + bytes-values: 115 + - match: \b(\d+)([KMG])(?=[\s,"]) 116 + scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common 117 + captures: 118 + 1: constant.numeric.value.ssh.common 119 + 2: constant.numeric.suffix.ssh.common 120 + mac-addresses: 121 + - match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2}) 122 + scope: entity.name.constant.mac-address.ssh.common 123 + ipv4: 124 + - match: \b{{ipv4}}\b 125 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 126 + ipv6: 127 + - match: '{{ipv6}}' 128 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 129 + ipv6-square-bracket: 130 + - match: (\[){{ipv6}}(\]) 131 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 132 + captures: 133 + 1: punctuation.definition.constant.begin.ssh.common 134 + 2: punctuation.definition.constant.end.ssh.common 135 + ip-addresses: 136 + - include: ipv6 137 + - include: ipv4 138 + ipv4-with-cidr: 139 + - match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b 140 + captures: 141 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 142 + 2: punctuation.separator.sequence.ssh.common 143 + 3: constant.other.range.ssh.common 144 + ipv6-with-cidr: 145 + - match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)? 146 + captures: 147 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 148 + 2: punctuation.separator.sequence.ssh.common 149 + 3: constant.other.range.ssh.common 150 + ip-addresses-with-cidr: 151 + - include: ipv6-with-cidr 152 + - include: ipv4-with-cidr 153 + port-numbers: 154 + - match: \b{{zero_to_65535}}(?![\w:]) 155 + scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common 156 + match-all: 157 + - match: '\b(?xi: all )\b' 158 + scope: constant.language.boolean.true.ssh.common 159 + none: 160 + - match: \bnone\b 161 + scope: constant.language.null.ssh.common 162 + any: 163 + - match: \bany\b 164 + scope: constant.language.set.ssh.common 165 + boolean: 166 + - match: \byes\b 167 + scope: constant.language.boolean.true.ssh.common 168 + - match: \bno\b 169 + scope: constant.language.boolean.false.ssh.common 170 + boolean-with-typing: 171 + - include: boolean 172 + - match: \b(?:ye?|n)\b 173 + log-level: 174 + - match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b' 175 + scope: constant.language.log-level.ssh.common 176 + possibly-quoted-value: 177 + - meta_content_scope: meta.mapping.value.ssh.common 178 + - match: '"' 179 + scope: punctuation.definition.string.begin.ssh.common 180 + push: 181 + - meta_scope: string.quoted.double.ssh.common 182 + - match: (")(?:\s*(\S.*))? 183 + captures: 184 + 1: punctuation.definition.string.end.ssh.common 185 + 2: invalid.illegal.ssh.common 186 + pop: 1 187 + - match: \n|$ 188 + scope: invalid.illegal.unclosed-string.ssh.common 189 + pop: 2 190 + - match: (?=\S) 191 + push: 192 + - meta_content_scope: string.unquoted.ssh.common 193 + - include: pop-before-nl 194 + - include: pop-nl 195 + string-patterns: 196 + - include: punctuation-comma-sequence 197 + - include: operator-exclamation 198 + - match: '"' 199 + scope: punctuation.definition.string.begin.ssh.common 200 + push: 201 + - meta_content_scope: string.quoted.double.ssh.common 202 + - match: '"' 203 + scope: punctuation.definition.string.end.ssh.common 204 + pop: 1 205 + - include: wildcards 206 + - match: (?=\S) 207 + push: 208 + - meta_content_scope: string.unquoted.ssh.common 209 + - match: (?=[,!\s]) 210 + pop: 1 211 + - include: wildcards 212 + paths: 213 + - match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?) 214 + push: 215 + - meta_scope: meta.path.ssh.common entity.name.ssh.common 216 + - match: (?=[\s,"]) 217 + pop: 1 218 + - match: ~[\w\-.]* 219 + scope: variable.language.home.ssh.common 220 + - match: (/)(?:(\.{1,2})(?=/)|\.(?!/))? 221 + captures: 222 + 1: punctuation.separator.path.ssh.common 223 + 2: constant.other.placeholder.ssh.common 224 + - match: \.(?=[\w*?%]) 225 + scope: punctuation.separator.sequence.ssh.common 226 + - include: wildcards 227 + - include: tokens 228 + - include: environment-variables 229 + none-command-values: 230 + - match: \s*(none)\b[ \t]*$ 231 + captures: 232 + 1: constant.language.null.ssh.common 233 + - match: \s*((")(none)("))[ \t]*$ 234 + captures: 235 + 1: string.quoted.double.ssh.common 236 + 2: punctuation.definition.string.begin.ssh.common 237 + 3: constant.language.null.ssh.common 238 + 4: punctuation.definition.string.end.ssh.common 239 + tokens: [] 240 + environment-variables: [] 241 + pop-nl: 242 + - match: \n 243 + pop: 1 244 + pop-before-nl: 245 + - match: (?=\n) 246 + pop: 1 247 + ssh-ciphers: 248 + - match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 249 + scope: support.function.cipher.ssh.crypto 250 + - match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"]) 251 + scope: invalid.deprecated.cipher.ssh.crypto 252 + ssh-kex-algorithms: 253 + - match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"]) 254 + scope: support.function.kex-algorithm.ssh.crypto 255 + - match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"]) 256 + scope: invalid.deprecated.kex-algorithm.ssh.crypto 257 + ssh-key-types: 258 + - match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"]) 259 + scope: support.type.key-type.ssh.crypto 260 + - match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"]) 261 + scope: invalid.deprecated.key-type.ssh.crypto 262 + ssh-mac-algorithms: 263 + - match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 264 + scope: support.function.mac-algorithm.ssh.crypto 265 + - match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"]) 266 + scope: invalid.deprecated.mac-algorithm.ssh.crypto 267 + hostname-or-ip-value: 268 + - include: operator-exclamation 269 + - match: \[ 270 + scope: punctuation.definition.string.begin.known_hosts 271 + push: 272 + - meta_scope: meta.brackets.host.known_hosts 273 + - match: (\])(?:(:)({{zero_to_65535}}))? 274 + captures: 275 + 1: punctuation.definition.string.end.known_hosts 276 + 2: punctuation.separator.sequence.known_hosts 277 + 3: meta.number.integer.decimal.known_hosts constant.numeric.port-number.known_hosts 278 + pop: 1 279 + - include: operator-exclamation 280 + - include: ip-addresses 281 + - match: '' 282 + push: 283 + - meta_scope: meta.string.host.known_hosts string.quoted.other.known_hosts 284 + - match: (?=,|\]) 285 + pop: 1 286 + - include: wildcards 287 + - include: punctuation-dot-sequence 288 + - include: ip-addresses 289 + - match: (\|)(\d+)(\|)({{base64_char}}{27}=)(\|)({{base64_char}}{27}=) 290 + scope: meta.string.host.obfuscated.known_hosts 291 + captures: 292 + 1: punctuation.definition.known_hosts 293 + 2: constant.numeric.integer.algorithm.known_hosts 294 + 3: punctuation.definition.known_hosts 295 + 4: string.unquoted.salt.known_hosts 296 + 5: punctuation.definition.known_hosts 297 + 6: string.unquoted.hash.known_hosts 298 + - match: (?=\S) 299 + push: hostname 300 + hostname: 301 + - meta_content_scope: meta.string.host.known_hosts string.unquoted.known_hosts 302 + - match: (?=[,\[\s]) 303 + pop: 1 304 + - include: wildcards 305 + - include: punctuation-dot-sequence
+151
syntaxes/pem.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # Not strictly just PEM. Includes some other stuff, just to be helpful. 4 + 5 + # https://www.sublimetext.com/docs/syntax.html 6 + # https://datatracker.ietf.org/doc/html/rfc7468 (PEM) 7 + # https://datatracker.ietf.org/doc/html/rfc4716 (OpenSSH) 8 + # https://datatracker.ietf.org/doc/html/rfc4880 (OpenPGP) 9 + 10 + name: Private Encrypted Mail (PEM) Key 11 + scope: source.pem 12 + version: 2 13 + extends: SSH Common.sublime-syntax 14 + 15 + file_extensions: 16 + - pem 17 + 18 + hidden_file_extensions: 19 + - cer 20 + - cert 21 + - crt 22 + - id_dsa 23 + - id_ed25519 24 + - id_ed448 25 + - id_eddsa 26 + - id_rsa 27 + 28 + first_line_match: |- 29 + ^(?x: 30 + (-{4}[ -]) 31 + BEGIN [ ] 32 + ( (?:[0-9A-Z -]+[ ])? (?: PUBLIC | PRIVATE ) [ ] KEY 33 + | (?:[0-9A-Z -]+[ ])? CERTIFICATE (?:[ ] REQUEST )? 34 + | (?:[0-9A-Z -]+[ ])? PARAMETERS 35 + | X509 [ ] CRL 36 + | PKCS7 37 + | PKCS [ ] \#7 [ ] SIGNED [ ] DATA 38 + | CMS 39 + | PGP [ ] MESSAGE (?:,[ ] PART [ ] \d+(?:/\d+)?)? 40 + | PGP [ ] (?: PUBLIC | PRIVATE ) [ ] KEY [ ] BLOCK 41 + | PGP [ ] SIGNATURE 42 + ) 43 + ([ -]-{4}) 44 + ) 45 + 46 + contexts: 47 + main: 48 + - include: comments-number-sign 49 + - match: |- 50 + ^(?x: 51 + (-{4}[ -]) 52 + BEGIN [ ] 53 + ( (?:[0-9A-Z -]+[ ])? (?: PUBLIC | PRIVATE ) [ ] KEY 54 + | (?:[0-9A-Z -]+[ ])? CERTIFICATE (?:[ ] REQUEST )? 55 + | (?:[0-9A-Z -]+[ ])? PARAMETERS 56 + | X509 [ ] CRL 57 + | PKCS7 58 + | PKCS [ ] \#7 [ ] SIGNED [ ] DATA 59 + | CMS 60 + | PGP [ ] MESSAGE (?:,[ ] PART [ ] \d+(?:/\d+)?)? 61 + | PGP [ ] (?: PUBLIC | PRIVATE ) [ ] KEY [ ] BLOCK 62 + | PGP [ ] SIGNATURE 63 + ) 64 + ([ -]-{4}) 65 + ) 66 + scope: punctuation.section.block.begin.pem 67 + push: pem-key 68 + - include: setext-headings 69 + 70 + pem-key: 71 + - meta_scope: meta.block.pem 72 + - match: ^\1END \2\3 73 + scope: punctuation.section.block.end.pem 74 + pop: 1 75 + - include: comments-number-sign 76 + - match: ^{{base64_char}}{1,100}(={0,3})?$ 77 + scope: string.unquoted.pem 78 + captures: 79 + 1: punctuation.definition.string.end.pem 80 + - include: headers 81 + 82 + headers: 83 + - match: ^(?i:(Comment))(:) 84 + captures: 85 + 1: keyword.other.comment.pem 86 + 2: punctuation.separator.key-value.pem 87 + push: 88 + - meta_content_scope: comment.line.pem 89 + - include: header-end 90 + - match: ^((x-)?[\w-]+)(:) 91 + captures: 92 + 1: meta.mapping.key.pem keyword.other.pem 93 + 2: variable.annotation.pem 94 + 3: punctuation.separator.key-value.pem 95 + push: header-value 96 + 97 + header-value: 98 + - meta_scope: meta.mapping.pem 99 + - meta_content_scope: meta.mapping.value.pem 100 + - include: header-end 101 + - include: punctuation-comma-sequence 102 + - match: = 103 + scope: punctuation.separator.key-value.pem 104 + - match: '\b(?x: ENCRYPTED | MIC-ONLY | MIC-CLEAR )\b' 105 + scope: storage.modifier.pem 106 + - match: |- 107 + \b(?x: 108 + ( AES-(?:128|256)-CBC 109 + | DES-(?:EDE3-)?CBC 110 + )\b 111 + ( (,) .+ )? 112 + ) 113 + captures: 114 + 1: meta.function-call.identifier.pem 115 + support.function.cipher.ssh.crypto 116 + 2: meta.function-call.arguments.pem 117 + 3: punctuation.section.arguments.begin.pem 118 + 119 + header-end: 120 + - match: \\\r?\n 121 + scope: punctuation.separator.continuation.line.pem 122 + push: 123 + - match: ^ 124 + pop: 1 125 + - match: (?=$) 126 + pop: 1 127 + 128 + setext-headings: 129 + - match: ^(?:=+|(?=\S)) 130 + branch_point: maybe-heading 131 + branch: 132 + - setext-heading 133 + - not-heading 134 + 135 + setext-heading: 136 + - meta_scope: markup.heading.pem 137 + - meta_content_scope: entity.name.section.pem 138 + - match: ^(={5,})[ \t]*$(\n?) 139 + captures: 140 + 1: punctuation.definition.heading.setext.pem 141 + 2: meta.whitespace.newline.pem 142 + pop: 1 143 + - match: ^(?!=+)$ 144 + fail: maybe-heading 145 + 146 + not-heading: 147 + - match: '' 148 + pop: 1 149 + 150 + variables: 151 + base64_char: '[a-zA-Z0-9+/]'
+288
syntaxes/ssh-common.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # This file is some kind of internal library which is used to store 4 + # common rules which are used by the visible syntax files. 5 + name: SSH Common 6 + scope: text.ssh.common 7 + version: 2 8 + hidden: true 9 + 10 + contexts: 11 + main: 12 + - include: comments-number-sign 13 + 14 + ###[ COMMENTS ]################################################################ 15 + 16 + comments: 17 + - include: comments-number-sign 18 + - include: comments-semicolon 19 + 20 + comments-number-sign: 21 + - match: ^\s*(#+) 22 + captures: 23 + 1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common 24 + push: 25 + - meta_content_scope: comment.line.number-sign.ssh.common 26 + - match: \n 27 + scope: comment.line.number-sign.ssh.common 28 + pop: true 29 + 30 + comments-semicolon: 31 + - match: ^\s*(;+) 32 + captures: 33 + 1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common 34 + push: 35 + - meta_content_scope: comment.line.semi-colon.ssh.common 36 + - include: pop-nl 37 + 38 + ###[ COMPONENTS ]############################################################## 39 + 40 + operator-exclamation: 41 + - match: '!' 42 + scope: keyword.operator.logical.ssh.common 43 + 44 + wildcards: 45 + - match: \* 46 + scope: constant.other.wildcard.asterisk.ssh.common 47 + - match: \? 48 + scope: constant.other.wildcard.questionmark.ssh.common 49 + 50 + punctuation-comma-sequence: 51 + - match: ',' 52 + scope: punctuation.separator.sequence.ssh.common 53 + 54 + punctuation-dot-sequence: 55 + - match: \. 56 + scope: punctuation.separator.sequence.ssh.common 57 + 58 + punctuation-at: 59 + - match: '@' 60 + scope: punctuation.separator.sequence.ssh.common 61 + 62 + ssh-fingerprint: 63 + - match: '{{ssh_fingerprint}}' 64 + scope: variable.other.fingerprint.ssh.common 65 + 66 + ssh-fingerprint-with-label: 67 + - match: '{{ssh_fingerprint}}' 68 + scope: variable.other.fingerprint.ssh.common 69 + push: expect-fingerprint-label 70 + 71 + expect-fingerprint-label: 72 + - include: pop-before-nl 73 + - match: (?=\S) 74 + push: 75 + - meta_scope: meta.annotation.identifier.ssh.common 76 + string.unquoted.ssh.common 77 + - match: '(?=[ \t]*$)' 78 + pop: 1 79 + - include: punctuation-at 80 + 81 + time-values: 82 + # https://man.openbsd.org/sshd_config.5#TIME_FORMATS 83 + # seconds, minutes, hours, days, weeks 84 + - match: \b(?=[\dsmhdw]*\d[smhdw][\s,"]) 85 + push: 86 + - meta_scope: meta.constant.time.ssh.common 87 + meta.number.integer.decimal.ssh.common 88 + - match: (?=[\s,"]) 89 + pop: 1 90 + - match: (\d+)([smhdw]) 91 + captures: 92 + 1: constant.numeric.value.ssh.common 93 + 2: constant.numeric.suffix.ssh.common 94 + 95 + bytes-values: 96 + - match: \b(\d+)([KMG])(?=[\s,"]) 97 + scope: meta.constant.bytes.ssh.common 98 + meta.number.integer.other.ssh.common 99 + captures: 100 + 1: constant.numeric.value.ssh.common 101 + 2: constant.numeric.suffix.ssh.common 102 + 103 + mac-addresses: 104 + - match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2}) 105 + scope: entity.name.constant.mac-address.ssh.common 106 + 107 + ipv4: 108 + - match: '\b{{ipv4}}\b' 109 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 110 + 111 + ipv6: 112 + - match: '{{ipv6}}' 113 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 114 + 115 + ipv6-square-bracket: 116 + - match: (\[){{ipv6}}(\]) 117 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 118 + captures: 119 + 1: punctuation.definition.constant.begin.ssh.common 120 + 2: punctuation.definition.constant.end.ssh.common 121 + 122 + ip-addresses: 123 + - include: ipv6 124 + - include: ipv4 125 + 126 + ipv4-with-cidr: 127 + - match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b 128 + captures: 129 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 130 + 2: punctuation.separator.sequence.ssh.common 131 + 3: constant.other.range.ssh.common 132 + 133 + ipv6-with-cidr: 134 + - match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)? 135 + captures: 136 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 137 + 2: punctuation.separator.sequence.ssh.common 138 + 3: constant.other.range.ssh.common 139 + 140 + ip-addresses-with-cidr: 141 + - include: ipv6-with-cidr 142 + - include: ipv4-with-cidr 143 + 144 + port-numbers: 145 + - match: \b{{zero_to_65535}}(?![\w:]) 146 + scope: meta.number.integer.decimal.ssh.common 147 + constant.numeric.port-number.ssh.common 148 + 149 + match-all: 150 + - match: '\b(?xi: all )\b' 151 + scope: constant.language.boolean.true.ssh.common 152 + 153 + none: 154 + - match: \bnone\b 155 + scope: constant.language.null.ssh.common 156 + 157 + any: 158 + - match: \bany\b 159 + scope: constant.language.set.ssh.common 160 + 161 + boolean: 162 + - match: \byes\b 163 + scope: constant.language.boolean.true.ssh.common 164 + - match: \bno\b 165 + scope: constant.language.boolean.false.ssh.common 166 + 167 + boolean-with-typing: 168 + - include: boolean 169 + # Consume while typing as well, but unscoped 170 + - match: \b(?:ye?|n)\b 171 + 172 + log-level: 173 + - match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b' 174 + scope: constant.language.log-level.ssh.common 175 + 176 + possibly-quoted-value: 177 + - meta_content_scope: meta.mapping.value.ssh.common 178 + - match: '"' 179 + scope: punctuation.definition.string.begin.ssh.common 180 + push: 181 + - meta_scope: string.quoted.double.ssh.common 182 + - match: (")(?:\s*(\S.*))? 183 + captures: 184 + 1: punctuation.definition.string.end.ssh.common 185 + 2: invalid.illegal.ssh.common 186 + pop: 1 187 + - match: \n|$ 188 + scope: invalid.illegal.unclosed-string.ssh.common 189 + pop: 2 190 + - match: (?=\S) 191 + push: 192 + - meta_content_scope: string.unquoted.ssh.common 193 + - include: pop-before-nl 194 + - include: pop-nl 195 + 196 + string-patterns: 197 + # https://man7.org/linux/man-pages/man5/ssh_config.5.html#PATTERNS 198 + # https://man.openbsd.org/ssh_config.5#PATTERNS 199 + # https://man7.org/linux/man-pages/man5/sshd_config.5.html#PATTERNS 200 + # https://man.openbsd.org/sshd_config.5#PATTERNS 201 + - include: punctuation-comma-sequence 202 + - include: operator-exclamation 203 + - match: '"' 204 + scope: punctuation.definition.string.begin.ssh.common 205 + push: 206 + - meta_content_scope: string.quoted.double.ssh.common 207 + - match: '"' 208 + scope: punctuation.definition.string.end.ssh.common 209 + pop: 1 210 + - include: wildcards 211 + - match: (?=\S) 212 + push: 213 + - meta_content_scope: string.unquoted.ssh.common 214 + - match: (?=[,!\s]) 215 + pop: 1 216 + - include: wildcards 217 + 218 + paths: 219 + # This is just heuristic. Expect failures. 220 + - match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?) 221 + push: 222 + - meta_scope: meta.path.ssh.common 223 + entity.name.ssh.common 224 + - match: (?=[\s,"]) 225 + pop: 1 226 + - match: ~[\w\-.]* 227 + scope: variable.language.home.ssh.common 228 + - match: (/)(?:(\.{1,2})(?=/)|\.(?!/))? 229 + captures: 230 + 1: punctuation.separator.path.ssh.common 231 + 2: constant.other.placeholder.ssh.common 232 + - match: \.(?=[\w*?%]) 233 + scope: punctuation.separator.sequence.ssh.common 234 + - include: wildcards 235 + - include: tokens 236 + - include: environment-variables 237 + 238 + none-command-values: 239 + - match: \s*(none)\b[ \t]*$ 240 + captures: 241 + 1: constant.language.null.ssh.common 242 + - match: \s*((")(none)("))[ \t]*$ 243 + captures: 244 + 1: string.quoted.double.ssh.common 245 + 2: punctuation.definition.string.begin.ssh.common 246 + 3: constant.language.null.ssh.common 247 + 4: punctuation.definition.string.end.ssh.common 248 + 249 + tokens: [] 250 + environment-variables: [] 251 + 252 + ###[ PROTOTYPE ]############################################################### 253 + 254 + pop-nl: 255 + - match: \n 256 + pop: 1 257 + 258 + pop-before-nl: 259 + - match: (?=\n) 260 + pop: 1 261 + 262 + ############################################################################### 263 + 264 + variables: 265 + base64_char: '[a-zA-Z0-9+/]' 266 + ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3}) 267 + # ipv4_basic: (?:(?:\d{1,3}\.){3}\d{1,3}) 268 + # ipv6_basic: (?i:(?:[a-f0-9:]+:+)+[a-f0-9]+) 269 + zero_to_32: (?:3[0-2]|[12][0-9]|[0-9]) 270 + zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9]) 271 + zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9]) 272 + zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9]) 273 + ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}}) 274 + ipv6: |- 275 + (?xi: 276 + (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) 277 + |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address) 278 + |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,}) # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index) 279 + |(?:(?:[0-9a-f]{1,4}:){7,7} [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8 280 + | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6}) # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 281 + |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}) # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 282 + |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}) # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 283 + |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}) # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 284 + |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}) # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 285 + |(?:(?:[0-9a-f]{1,4}:){1,6} :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 286 + |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: 1:2:3:4:5:6:7:: 287 + |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: 288 + )
+556
syntaxes/ssh-config.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # Standalone version of ssh-config.sublime-syntax 4 + # Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax 5 + 6 + name: SSH Config 7 + scope: source.ssh_config 8 + version: 2 9 + file_extensions: 10 + - ssh_config 11 + variables: 12 + base64_char: '[a-zA-Z0-9+/]' 13 + ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3}) 14 + zero_to_32: (?:3[0-2]|[12][0-9]|[0-9]) 15 + zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9]) 16 + zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9]) 17 + zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9]) 18 + ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}}) 19 + ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\ 20 + \ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\ 21 + \ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \ 22 + \ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \ 23 + \ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\ 24 + \ # fe80::7:8%eth0 fe80::7:8%1 \ 25 + \ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\ 26 + \ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\ 27 + \ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\ 28 + \ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\ 29 + \ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\ 30 + \ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\ 31 + \ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\ 32 + \ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\ 33 + \ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \ 34 + \ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \ 35 + \ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)" 36 + tokens_standard: (?:%[%CdhikLlnpru]) 37 + tokens_knownhosts: (?:{{tokens_standard}}|%[%fHIKt]) 38 + tokens_hostname: (?:%[%h]) 39 + tokens_proxycommand: (?:%[%hnpr]) 40 + tokens_all: (?:{{tokens_knownhosts}}|%T) 41 + tokens_localcommand: '{{tokens_all}}' 42 + match_parameters: "\\b(?xi:\n all | canonical | final | exec | localnetwork | host\ 43 + \ | originalhost\n| tagged | command | user | localuser | version | sessiontype\n\ 44 + )\\b" 45 + contexts: 46 + main: 47 + - include: comments 48 + - include: host-block 49 + - include: match 50 + - include: naked-parameters 51 + comments: 52 + - include: comments-number-sign 53 + - include: comments-semicolon 54 + comments-number-sign: 55 + - match: ^\s*(#+) 56 + captures: 57 + 1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common 58 + push: 59 + - meta_content_scope: comment.line.number-sign.ssh.common 60 + - match: \n 61 + scope: comment.line.number-sign.ssh.common 62 + pop: true 63 + comments-semicolon: 64 + - match: ^\s*(;+) 65 + captures: 66 + 1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common 67 + push: 68 + - meta_content_scope: comment.line.semi-colon.ssh.common 69 + - include: pop-nl 70 + operator-exclamation: 71 + - match: '!' 72 + scope: keyword.operator.logical.ssh.common 73 + wildcards: 74 + - match: \* 75 + scope: constant.other.wildcard.asterisk.ssh.common 76 + - match: \? 77 + scope: constant.other.wildcard.questionmark.ssh.common 78 + punctuation-comma-sequence: 79 + - match: ',' 80 + scope: punctuation.separator.sequence.ssh.common 81 + punctuation-dot-sequence: 82 + - match: \. 83 + scope: punctuation.separator.sequence.ssh.common 84 + punctuation-at: 85 + - match: '@' 86 + scope: punctuation.separator.sequence.ssh.common 87 + ssh-fingerprint: 88 + - match: '{{ssh_fingerprint}}' 89 + scope: variable.other.fingerprint.ssh.common 90 + ssh-fingerprint-with-label: 91 + - match: '{{ssh_fingerprint}}' 92 + scope: variable.other.fingerprint.ssh.common 93 + push: expect-fingerprint-label 94 + expect-fingerprint-label: 95 + - include: pop-before-nl 96 + - match: (?=\S) 97 + push: 98 + - meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common 99 + - match: (?=[ \t]*$) 100 + pop: 1 101 + - include: punctuation-at 102 + time-values: 103 + - match: \b(?=[\dsmhdw]*\d[smhdw][\s,"]) 104 + push: 105 + - meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common 106 + - match: (?=[\s,"]) 107 + pop: 1 108 + - match: (\d+)([smhdw]) 109 + captures: 110 + 1: constant.numeric.value.ssh.common 111 + 2: constant.numeric.suffix.ssh.common 112 + bytes-values: 113 + - match: \b(\d+)([KMG])(?=[\s,"]) 114 + scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common 115 + captures: 116 + 1: constant.numeric.value.ssh.common 117 + 2: constant.numeric.suffix.ssh.common 118 + mac-addresses: 119 + - match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2}) 120 + scope: entity.name.constant.mac-address.ssh.common 121 + ipv4: 122 + - match: \b{{ipv4}}\b 123 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 124 + ipv6: 125 + - match: '{{ipv6}}' 126 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 127 + ipv6-square-bracket: 128 + - match: (\[){{ipv6}}(\]) 129 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 130 + captures: 131 + 1: punctuation.definition.constant.begin.ssh.common 132 + 2: punctuation.definition.constant.end.ssh.common 133 + ip-addresses: 134 + - include: ipv6 135 + - include: ipv4 136 + ipv4-with-cidr: 137 + - match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b 138 + captures: 139 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 140 + 2: punctuation.separator.sequence.ssh.common 141 + 3: constant.other.range.ssh.common 142 + ipv6-with-cidr: 143 + - match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)? 144 + captures: 145 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 146 + 2: punctuation.separator.sequence.ssh.common 147 + 3: constant.other.range.ssh.common 148 + ip-addresses-with-cidr: 149 + - include: ipv6-with-cidr 150 + - include: ipv4-with-cidr 151 + port-numbers: 152 + - match: \b{{zero_to_65535}}(?![\w:]) 153 + scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common 154 + match-all: 155 + - match: '\b(?xi: all )\b' 156 + scope: constant.language.boolean.true.ssh.common 157 + none: 158 + - match: \bnone\b 159 + scope: constant.language.null.ssh.common 160 + any: 161 + - match: \bany\b 162 + scope: constant.language.set.ssh.common 163 + boolean: 164 + - match: \byes\b 165 + scope: constant.language.boolean.true.ssh.common 166 + - match: \bno\b 167 + scope: constant.language.boolean.false.ssh.common 168 + boolean-with-typing: 169 + - include: boolean 170 + - match: \b(?:ye?|n)\b 171 + log-level: 172 + - match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b' 173 + scope: constant.language.log-level.ssh.common 174 + possibly-quoted-value: 175 + - meta_content_scope: meta.mapping.value.ssh_config 176 + - match: '"' 177 + scope: punctuation.definition.string.begin.ssh_config 178 + push: 179 + - meta_scope: string.quoted.double.ssh_config 180 + - match: (")(?:\s*(\S.*))? 181 + captures: 182 + 1: punctuation.definition.string.end.ssh_config 183 + 2: invalid.illegal.ssh_config 184 + pop: 1 185 + - match: \n|$ 186 + scope: invalid.illegal.unclosed-string.ssh_config 187 + pop: 2 188 + - match: (?=\S) 189 + push: 190 + - meta_content_scope: string.unquoted.ssh_config 191 + - include: pop-before-nl 192 + - include: pop-nl 193 + string-patterns: 194 + - include: punctuation-comma-sequence 195 + - include: operator-exclamation 196 + - match: '"' 197 + scope: punctuation.definition.string.begin.ssh.common 198 + push: 199 + - meta_content_scope: string.quoted.double.ssh.common 200 + - match: '"' 201 + scope: punctuation.definition.string.end.ssh.common 202 + pop: 1 203 + - include: wildcards 204 + - match: (?=\S) 205 + push: 206 + - meta_content_scope: string.unquoted.ssh.common 207 + - match: (?=[,!\s]) 208 + pop: 1 209 + - include: wildcards 210 + paths: 211 + - match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?) 212 + push: 213 + - meta_scope: meta.path.ssh.common entity.name.ssh.common 214 + - match: (?=[\s,"]) 215 + pop: 1 216 + - match: ~[\w\-.]* 217 + scope: variable.language.home.ssh.common 218 + - match: (/)(?:(\.{1,2})(?=/)|\.(?!/))? 219 + captures: 220 + 1: punctuation.separator.path.ssh.common 221 + 2: constant.other.placeholder.ssh.common 222 + - match: \.(?=[\w*?%]) 223 + scope: punctuation.separator.sequence.ssh.common 224 + - include: wildcards 225 + - include: tokens 226 + - include: environment-variables 227 + none-command-values: 228 + - match: \s*(none)\b[ \t]*$ 229 + captures: 230 + 1: constant.language.null.ssh.common 231 + - match: \s*((")(none)("))[ \t]*$ 232 + captures: 233 + 1: string.quoted.double.ssh.common 234 + 2: punctuation.definition.string.begin.ssh.common 235 + 3: constant.language.null.ssh.common 236 + 4: punctuation.definition.string.end.ssh.common 237 + tokens: 238 + - match: '%%' 239 + scope: constant.character.escape.ssh_config 240 + - match: '{{tokens_standard}}' 241 + scope: constant.other.placeholder.ssh_config 242 + environment-variables: 243 + - include: scope:source.shell#parameter-expansions 244 + pop-nl: 245 + - match: \n 246 + pop: 1 247 + pop-before-nl: 248 + - match: (?=\n) 249 + pop: 1 250 + ssh-ciphers: 251 + - match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 252 + scope: support.function.cipher.ssh.crypto 253 + - match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"]) 254 + scope: invalid.deprecated.cipher.ssh.crypto 255 + ssh-kex-algorithms: 256 + - match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"]) 257 + scope: support.function.kex-algorithm.ssh.crypto 258 + - match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"]) 259 + scope: invalid.deprecated.kex-algorithm.ssh.crypto 260 + ssh-key-types: 261 + - match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"]) 262 + scope: support.type.key-type.ssh.crypto 263 + - match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"]) 264 + scope: invalid.deprecated.key-type.ssh.crypto 265 + ssh-mac-algorithms: 266 + - match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 267 + scope: support.function.mac-algorithm.ssh.crypto 268 + - match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"]) 269 + scope: invalid.deprecated.mac-algorithm.ssh.crypto 270 + parameters: 271 + - include: comments 272 + - include: parameter-hostname 273 + - include: parameter-localcommand 274 + - include: parameter-proxycommand 275 + - include: parameter-proxyjump 276 + - include: parameter-knownhostscommand 277 + - include: parameter-with-boolean-values 278 + - include: parameter-with-boolean-values-plus-ask 279 + - include: parameter-generic 280 + pop-before-match-parameter: 281 + - include: pop-before-nl 282 + - match: (?=\s*{{match_parameters}}) 283 + pop: 1 284 + pop-before-next-host: 285 + - match: '(?=^\s*(?xi: Host | Match )\b)' 286 + pop: 1 287 + naked-parameters: 288 + - match: (?=\S) 289 + push: 290 + - meta_scope: meta.block.naked.ssh_config 291 + - include: pop-before-next-host 292 + - include: parameters 293 + host-pattern: 294 + - meta_content_scope: entity.name.label.host-alias.ssh_config 295 + - include: punctuation-dot-sequence 296 + - include: wildcards 297 + - match: (?=\s|,) 298 + pop: 1 299 + host-patterns: 300 + - include: operator-exclamation 301 + - include: punctuation-comma-sequence 302 + - match: (?=\S) 303 + push: host-pattern 304 + host-block: 305 + - match: ^\s*((?i:Host))\b 306 + captures: 307 + 1: keyword.declaration.host-alias.ssh_config 308 + set: host-aliases 309 + host-aliases: 310 + - meta_scope: meta.block.host.ssh_config 311 + - match: (?=\n) 312 + set: host-body 313 + - include: host-patterns 314 + host-body: 315 + - meta_scope: meta.block.host.ssh_config 316 + - include: pop-before-next-host 317 + - include: parameters 318 + match: 319 + - match: ^\s*((?i:Match))\b 320 + captures: 321 + 1: keyword.control.conditional.ssh_config 322 + set: match-conditions 323 + match-conditions: 324 + - meta_scope: meta.block.match.ssh_config 325 + - meta_content_scope: meta.statement.conditional.ssh_config 326 + - match: \n 327 + set: match-body 328 + - include: operator-exclamation 329 + - include: match-all 330 + - match: '\b(?xi: canonical | final )\b' 331 + scope: keyword.other.ssh_config 332 + - match: '\b((?xi: exec ))\b\s*(\")' 333 + captures: 334 + 1: keyword.other.ssh_config 335 + 2: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config 336 + escape: (?<!\\)\"(?=\s*(?:#.*)?) 337 + escape_captures: 338 + 0: meta.block.match.ssh_config meta.statement.conditional.ssh_config string.quoted.double.ssh_config 339 + punctuation.definition.string.end.ssh_config 340 + embed_scope: string.quoted.double.ssh_config 341 + embed: scope:source.shell.embedded.ssh 342 + - match: '\b((?xi: exec ))\b[ \t]+' 343 + captures: 344 + 1: keyword.other.ssh_config 345 + embed: scope:source.shell.embedded.ssh 346 + escape: (?=\s*(?:{{match_parameters}}|$)) 347 + - match: '\b(?xi: (?:original)? host )\b' 348 + scope: keyword.other.ssh_config 349 + push: 350 + - include: pop-before-match-parameter 351 + - include: punctuation-comma-sequence 352 + - include: host-patterns 353 + - match: '\b(?xi: (?:local)? user | tagged | version | command | sessiontype )\b' 354 + scope: keyword.other.ssh_config 355 + push: 356 + - include: pop-before-match-parameter 357 + - include: string-patterns 358 + - match: '\b(?xi: localnetwork )\b' 359 + scope: keyword.other.ssh_config 360 + push: 361 + - include: pop-before-match-parameter 362 + - include: punctuation-comma-sequence 363 + - include: ip-addresses-with-cidr 364 + match-body: 365 + - meta_content_scope: meta.block.match.ssh_config 366 + - include: pop-before-next-host 367 + - include: parameters 368 + parameter-hostname: 369 + - match: ^\s*((?i:HostName))\b\s*(=)? 370 + captures: 371 + 1: meta.mapping.key.ssh_config keyword.declaration.host.ssh_config 372 + 2: keyword.operator.assignment.ssh_config 373 + push: 374 + - meta_content_scope: meta.string.host.ssh_config 375 + - include: pop-nl 376 + - include: ip-addresses 377 + - match: (?=\S+) 378 + push: 379 + - meta_content_scope: string.unquoted.hostname.ssh_config 380 + - include: pop-before-nl 381 + - include: punctuation-dot-sequence 382 + - match: '{{tokens_hostname}}' 383 + scope: constant.character.escape.ssh_config 384 + - match: \s+(\S+) 385 + captures: 386 + 1: invalid.illegal.ssh_config 387 + parameter-proxyjump: 388 + - match: ^\s*((?i:ProxyJump))\b\s*(=)? 389 + captures: 390 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 391 + 2: keyword.operator.assignment.ssh_config 392 + push: 393 + - meta_content_scope: meta.mapping.value.ssh_config 394 + - include: pop-nl 395 + - include: none-command-values 396 + - match: '"' 397 + scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config 398 + escape: (")|(?=\n|$) 399 + escape_captures: 400 + 1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config 401 + embed_scope: string.quoted.double.ssh_config 402 + embed: proxyjump-values 403 + - match: (?=\S) 404 + escape: (?=\n|$) 405 + embed: proxyjump-values 406 + proxyjump-values: 407 + - include: ip-addresses 408 + - include: punctuation-comma-sequence 409 + - match: (?=[\w%]+@) 410 + push: 411 + - meta_content_scope: meta.string.user.ssh_config string.unquoted.ssh_config 412 + - match: '%%' 413 + scope: constant.character.escape.ssh_config 414 + - match: '{{tokens_proxycommand}}' 415 + scope: constant.other.placeholder.ssh_config 416 + - match: '@' 417 + scope: punctuation.separator.ssh_config 418 + pop: 1 419 + - match: :(?={{zero_to_65535}}(?![\w:])) 420 + scope: punctuation.separator.ssh_config 421 + push: 422 + - match: (?=\D) 423 + pop: 1 424 + - include: port-numbers 425 + - match: (?=\S+) 426 + push: 427 + - meta_content_scope: string.unquoted.hostname.ssh_config 428 + - match: (?=[\s,:"]) 429 + pop: 1 430 + - include: punctuation-dot-sequence 431 + - match: '%%' 432 + scope: constant.character.escape.ssh_config 433 + - match: '{{tokens_proxycommand}}' 434 + scope: constant.other.placeholder.ssh_config 435 + parameter-proxycommand: 436 + - match: ^\s*((?i:ProxyCommand))\b\s*(=)? 437 + captures: 438 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 439 + 2: keyword.operator.assignment.ssh_config 440 + push: 441 + - meta_content_scope: meta.mapping.value.ssh_config 442 + - include: pop-nl 443 + - include: none-command-values 444 + - match: '"' 445 + scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config 446 + escape: (")|(?=\n|$) 447 + escape_captures: 448 + 1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config 449 + embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.proxycommand 450 + embed: scope:source.shell.embedded.ssh.proxycommand 451 + - match: (?=\S) 452 + escape: (?=\n|$) 453 + embed: scope:source.shell.embedded.ssh.proxycommand 454 + parameter-localcommand: 455 + - match: ^\s*((?i:LocalCommand))\b\s*(=)? 456 + captures: 457 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 458 + 2: keyword.operator.assignment.ssh_config 459 + push: 460 + - meta_content_scope: meta.mapping.value.ssh_config 461 + - include: pop-nl 462 + - include: none-command-values 463 + - match: '"' 464 + scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config 465 + escape: (")|(?=$) 466 + escape_captures: 467 + 1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config 468 + embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.localcommand 469 + embed: scope:source.shell.embedded.ssh.localcommand 470 + - match: (?=\S) 471 + escape: (?=$) 472 + embed: scope:source.shell.embedded.ssh.localcommand 473 + parameter-knownhostscommand: 474 + - match: ^\s*((?i:KnownHostsCommand))\b\s*(=)? 475 + captures: 476 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 477 + 2: keyword.operator.assignment.ssh_config 478 + push: 479 + - meta_content_scope: meta.mapping.value.ssh_config 480 + - include: pop-nl 481 + - include: none-command-values 482 + - match: '"' 483 + scope: string.quoted.double.ssh_config punctuation.definition.string.begin.ssh_config 484 + escape: (")|(?=$) 485 + escape_captures: 486 + 1: meta.mapping.value.ssh_config string.quoted.double.ssh_config punctuation.definition.string.end.ssh_config 487 + embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.knownhostscommand 488 + embed: scope:source.shell.embedded.ssh.knownhostscommand 489 + - match: (?=\S) 490 + escape: (?=$) 491 + embed: scope:source.shell.embedded.ssh.knownhostscommand 492 + parameter-with-boolean-values: 493 + - match: "(?xi:\n ^\\s*\n (\n (?: Pubkey | HostBased | Password | ChallengeResponse\n\ 494 + \ | KbdInteractive | (?:Rhosts)? RSA\n ) Authentication # Auth\n | ForwardAgent\ 495 + \ | ForwardX11(?:Trusted)? | ClearAllForwardings\n | ExitOnForwardFailure #\ 496 + \ Fwds\n | BatchMode | CanonicalizeFallbackLocal | CheckHostIP | Compression\n\ 497 + \ | EnableEscapeCommandLine | EnableSSHKeySign\n | ForkAfterAuthentication\ 498 + \ | GatewayPorts | HashKnownHosts\n | IdentitiesOnly | NoHostAuthenticationForLocalhost\n\ 499 + \ | PermitLocalCommand | ProxyUseFdpass | RefuseConnection | StdinNull\n |\ 500 + \ StreamLocalBindUnlink | TCPKeepAlive\n | UseKeychain | UsePrivilegedPort\ 501 + \ | VisualHostKey\n | GSSAPI (?:\n Authentication | KeyExchange | DelegateCredentials\n\ 502 + \ | RenewalForcesRekey | TrustDNS ) # GSSAPI\n )\n \\b[ \\t]*(=)?\n)" 503 + captures: 504 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 505 + 2: keyword.operator.assignment.ssh_config 506 + with_prototype: 507 + - include: boolean-with-typing 508 + - match: '[^"\s]+' 509 + scope: invalid.illegal.sshd_config 510 + push: possibly-quoted-value 511 + parameter-with-boolean-values-plus-ask: 512 + - match: "(?xi:\n ^\\s*\n ( ControlMaster | StrictHostKeyChecking | UpdateHostKeys\n\ 513 + \ | VerifyHostKeyDNS\n )\n \\b[ \\t]*(=)?\n)" 514 + captures: 515 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 516 + 2: keyword.operator.assignment.ssh_config 517 + with_prototype: 518 + - include: boolean-with-typing 519 + - include: ask 520 + - match: \bas?\b 521 + - match: '[^"\s]+' 522 + scope: invalid.illegal.sshd_config 523 + push: possibly-quoted-value 524 + parameter-generic: 525 + - match: ^\s*([a-zA-Z1]+)\b[ \t]*(=)? 526 + captures: 527 + 1: meta.mapping.key.ssh_config keyword.other.ssh_config 528 + 2: keyword.operator.assignment.ssh_config 529 + with_prototype: 530 + - include: generic-parameter-values 531 + push: possibly-quoted-value 532 + ask: 533 + - match: \bask\b 534 + scope: constant.language.ssh_config 535 + generic-parameter-values: 536 + - include: environment-variables 537 + - include: none 538 + - include: boolean 539 + - include: any 540 + - include: ask 541 + - include: tokens 542 + - include: wildcards 543 + - include: operator-exclamation 544 + - include: punctuation-comma-sequence 545 + - include: ssh-key-types 546 + - include: ssh-ciphers 547 + - include: ssh-kex-algorithms 548 + - include: ssh-mac-algorithms 549 + - include: ipv6-square-bracket 550 + - include: ip-addresses-with-cidr 551 + - include: time-values 552 + - include: bytes-values 553 + - include: log-level 554 + - include: paths 555 + - match: \b\d+(?=[\s,"]) 556 + scope: meta.number.integer.ssh_config constant.numeric.value.ssh_config
+48
syntaxes/ssh-crypto.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + contexts: 4 + main: 5 + - include: comments 6 + - match: '^key type:' 7 + push: 8 + - include: pop-before-nl 9 + - include: ssh-key-types 10 + - match: '^cipher:' 11 + push: 12 + - include: pop-before-nl 13 + - include: ssh-ciphers 14 + - match: '^kex:' 15 + push: 16 + - include: pop-before-nl 17 + - include: ssh-kex-algorithms 18 + - match: '^mac:' 19 + push: 20 + - include: pop-before-nl 21 + - include: ssh-mac-algorithms 22 + ssh-ciphers: 23 + - match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 24 + scope: support.function.cipher.ssh.crypto 25 + - match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"]) 26 + scope: invalid.deprecated.cipher.ssh.crypto 27 + ssh-kex-algorithms: 28 + - match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"]) 29 + scope: support.function.kex-algorithm.ssh.crypto 30 + - match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"]) 31 + scope: invalid.deprecated.kex-algorithm.ssh.crypto 32 + ssh-key-types: 33 + - match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"]) 34 + scope: support.type.key-type.ssh.crypto 35 + - match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"]) 36 + scope: invalid.deprecated.key-type.ssh.crypto 37 + ssh-mac-algorithms: 38 + - match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 39 + scope: support.function.mac-algorithm.ssh.crypto 40 + - match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"]) 41 + scope: invalid.deprecated.mac-algorithm.ssh.crypto 42 + extends: SSH Common.sublime-syntax 43 + hidden: true 44 + hidden_file_extensions: 45 + - syntax_test_crypto 46 + name: SSH Crypto 47 + scope: text.ssh.crypto 48 + version: 2
+496
syntaxes/sshd-config.sublime-syntax
···
··· 1 + %YAML 1.2 2 + --- 3 + # Standalone version of sshd-config.sublime-syntax 4 + # Merged with: ssh-common.sublime-syntax, ssh-crypto.sublime-syntax 5 + 6 + name: SSHD Config 7 + scope: source.sshd_config 8 + version: 2 9 + file_extensions: 10 + - sshd_config 11 + variables: 12 + base64_char: '[a-zA-Z0-9+/]' 13 + ssh_fingerprint: (?:AAAA(?:E2V|[BC]3N){{base64_char}}+={0,3}) 14 + zero_to_32: (?:3[0-2]|[12][0-9]|[0-9]) 15 + zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9]) 16 + zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9]) 17 + zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9]) 18 + ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}}) 19 + ipv6: "(?xi:\n (?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255\ 20 + \ ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses\ 21 + \ and IPv4-translated addresses)\n |(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) \ 22 + \ # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 \ 23 + \ (IPv4-Embedded IPv6 Address)\n |(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,})\ 24 + \ # fe80::7:8%eth0 fe80::7:8%1 \ 25 + \ (link-local IPv6 addresses with zone index)\n |(?:(?:[0-9a-f]{1,4}:){7,7}\ 26 + \ [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8\n | (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6})\ 27 + \ # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8\n |(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5})\ 28 + \ # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8\n |(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4})\ 29 + \ # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8\n |(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3})\ 30 + \ # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8\n |(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2})\ 31 + \ # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8\n |(?:(?:[0-9a-f]{1,4}:){1,6}\ 32 + \ :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8\n\ 33 + \ |(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: \ 34 + \ 1:2:3:4:5:6:7::\n |(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) \ 35 + \ # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::\n)" 36 + all_parameters: "\\b(?xi:\n AcceptEnv | AddressFamily\n | Allow (?: AgentForwarding\ 37 + \ | Groups | StreamLocalForwarding\n | TcpForwarding | Users)\n | AuthenticationMethods\n\ 38 + \ | Authorized (?: Keys | Principals )(?: Command | CommandUser | File )\n |\ 39 + \ Banner\n | CASignatureAlgorithms | ChallengeResponseAuthentication\n | ChannelTimeout\ 40 + \ | ChrootDirectory | Ciphers | ClientAliveCountMax\n | ClientAliveInterval |\ 41 + \ Compression\n | DenyGroups | DenyUsers | DisableForwarding\n | ExposeAuthInfo\n\ 42 + \ | FingerprintHash | ForceCommand\n | GatewayPorts | GSSAPIAuthentication |\ 43 + \ GSSAPICleanupCredentials\n | GSSAPIStrictAcceptorCheck\n | Hostbased (?: AcceptedAlgorithms\ 44 + \ | AcceptedKeyTypes | Authentication\n | UsesNameFromPacketOnly\ 45 + \ )\n | HostCertificate | HostKey | HostKeyAgent | HostKeyAlgorithms\n | IgnoreRhosts\ 46 + \ | IgnoreUserKnownHosts | Include | IPQoS\n | KbdInteractiveAuthentication\n\ 47 + \ | Kerberos (?: Authentication | GetAFSToken | OrLocalPasswd\n |\ 48 + \ TicketCleanup )\n | KexAlgorithms | KeyRegenerationInterval\n | ListenAddress\ 49 + \ | LoginGraceTime | LogLevel | LogVerbose\n | MACs | Match | MaxAuthTries |\ 50 + \ MaxSessions | MaxStartups | ModuliFile\n | PasswordAuthentication | PAMServiceName\n\ 51 + \ | Permit (?: EmptyPasswords | Listen | Open | RootLogin | TTY | Tunnel\n \ 52 + \ | UserEnvironment | UserRC )\n | PerSource (?: MaxStartups | NetBlockSize\ 53 + \ | Penalties\n | PenaltyExemptList )\n | PidFile | Port | PrintLastLog\ 54 + \ | PrintMotd | Protocol\n | Pubkey (?: AcceptedAlgorithms | AcceptedKeyTypes\ 55 + \ | AuthOptions\n | Authentication )\n | RefuseConnection | RekeyLimit\ 56 + \ | RequiredRSASize | RevokedKeys | RDomain\n | RhostsRSAAuthentication | RSAAuthentication\n\ 57 + \ | SecurityKeyProvider | ServerKeyBits | SetEnv | ShowPatchLevel\n # SshdAuthPath\ 58 + \ and SshSessionPath are just for tests\n | StreamLocalBindMask | StreamLocalBindUnlink\n\ 59 + \ | StrictModes | Subsystem | SyslogFacility\n | TCPKeepAlive | TrustedUserCAKeys\n\ 60 + \ | UnusedConnectionTimeout | UseDNS | UseLogin | UsePAM\n | UsePrivilegeSeparation\n\ 61 + \ | VersionAddendum\n | X11DisplayOffset | X11Forwarding | X11UseLocalhost |\ 62 + \ XAuthLocation\n)\\b" 63 + parameters_boolean: "\\b(?xi:\n AllowAgentForwarding\n | ChallengeResponseAuthentication\ 64 + \ | Compression\n | ExposeAuthInfo\n | GSSAPIAuthentication | GSSAPICleanupCredentials\n\ 65 + \ | GSSAPIStrictAcceptorCheck\n | HostbasedAuthentication | HostbasedUsesNameFromPacketOnly\n\ 66 + \ | IgnoreRhosts | IgnoreUserKnownHosts\n | KbdInteractiveAuthentication | KerberosAuthentication\n\ 67 + \ | KerberosGetAFSToken | KerberosOrLocalPasswd\n | KerberosTicketCleanup\n\ 68 + \ | PasswordAuthentication | PermitEmptyPasswords | PermitTTY\n | PermitUserEnvironment\ 69 + \ | PermitUserRC | PrintLastLog | PrintMotd\n | PubkeyAuthentication\n | RefuseConnection\n\ 70 + \ | StreamLocalBindUnlink | StrictModes\n | TCPKeepAlive\n | UseDNS | UsePAM\n\ 71 + \ | X11Forwarding | X11UseLocalhost\n)\\b" 72 + contexts: 73 + main: 74 + - include: comments 75 + - include: match 76 + - include: parameters 77 + comments: 78 + - match: (#+)(?:\s*({{all_parameters}}))? 79 + captures: 80 + 1: punctuation.definition.comment.sshd_config 81 + 2: meta.keyword.comment.sshd_config 82 + push: 83 + - meta_scope: comment.line.number-sign.sshd_config 84 + - include: pop-nl 85 + - match: (;+)(?:\s*({{all_parameters}}))? 86 + captures: 87 + 1: punctuation.definition.comment.sshd_config 88 + 2: meta.keyword.comment.sshd_config 89 + push: 90 + - meta_scope: comment.line.semi-colon.sshd_config 91 + - include: pop-nl 92 + comments-number-sign: 93 + - match: ^\s*(#+) 94 + captures: 95 + 1: comment.line.number-sign.ssh.common punctuation.definition.comment.ssh.common 96 + push: 97 + - meta_content_scope: comment.line.number-sign.ssh.common 98 + - match: \n 99 + scope: comment.line.number-sign.ssh.common 100 + pop: true 101 + comments-semicolon: 102 + - match: ^\s*(;+) 103 + captures: 104 + 1: comment.line.semi-colon.ssh.common punctuation.definition.comment.ssh.common 105 + push: 106 + - meta_content_scope: comment.line.semi-colon.ssh.common 107 + - include: pop-nl 108 + operator-exclamation: 109 + - match: '!' 110 + scope: keyword.operator.logical.ssh.common 111 + wildcards: 112 + - match: \* 113 + scope: constant.other.wildcard.asterisk.ssh.common 114 + - match: \? 115 + scope: constant.other.wildcard.questionmark.ssh.common 116 + punctuation-comma-sequence: 117 + - match: ',' 118 + scope: punctuation.separator.sequence.ssh.common 119 + punctuation-dot-sequence: 120 + - match: \. 121 + scope: punctuation.separator.sequence.ssh.common 122 + punctuation-at: 123 + - match: '@' 124 + scope: punctuation.separator.sequence.ssh.common 125 + ssh-fingerprint: 126 + - match: '{{ssh_fingerprint}}' 127 + scope: variable.other.fingerprint.ssh.common 128 + ssh-fingerprint-with-label: 129 + - match: '{{ssh_fingerprint}}' 130 + scope: variable.other.fingerprint.ssh.common 131 + push: expect-fingerprint-label 132 + expect-fingerprint-label: 133 + - include: pop-before-nl 134 + - match: (?=\S) 135 + push: 136 + - meta_scope: meta.annotation.identifier.ssh.common string.unquoted.ssh.common 137 + - match: (?=[ \t]*$) 138 + pop: 1 139 + - include: punctuation-at 140 + time-values: 141 + - match: \b(?=[\dsmhdw]*\d[smhdw][\s,"]) 142 + push: 143 + - meta_scope: meta.constant.time.ssh.common meta.number.integer.decimal.ssh.common 144 + - match: (?=[\s,"]) 145 + pop: 1 146 + - match: (\d+)([smhdw]) 147 + captures: 148 + 1: constant.numeric.value.ssh.common 149 + 2: constant.numeric.suffix.ssh.common 150 + bytes-values: 151 + - match: \b(\d+)([KMG])(?=[\s,"]) 152 + scope: meta.constant.bytes.ssh.common meta.number.integer.other.ssh.common 153 + captures: 154 + 1: constant.numeric.value.ssh.common 155 + 2: constant.numeric.suffix.ssh.common 156 + mac-addresses: 157 + - match: (?:[0-9a-fA-F]{2}:){5}(?:[0-9a-fA-F]{2}) 158 + scope: entity.name.constant.mac-address.ssh.common 159 + ipv4: 160 + - match: \b{{ipv4}}\b 161 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 162 + ipv6: 163 + - match: '{{ipv6}}' 164 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 165 + ipv6-square-bracket: 166 + - match: (\[){{ipv6}}(\]) 167 + scope: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 168 + captures: 169 + 1: punctuation.definition.constant.begin.ssh.common 170 + 2: punctuation.definition.constant.end.ssh.common 171 + ip-addresses: 172 + - include: ipv6 173 + - include: ipv4 174 + ipv4-with-cidr: 175 + - match: \b({{ipv4}})(?:(/)({{zero_to_32}}))?\b 176 + captures: 177 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v4.ssh.common 178 + 2: punctuation.separator.sequence.ssh.common 179 + 3: constant.other.range.ssh.common 180 + ipv6-with-cidr: 181 + - match: ({{ipv6}})(?:(/)({{zero_to_128}})\b)? 182 + captures: 183 + 1: meta.number.integer.other.ssh.common constant.numeric.ip-address.v6.ssh.common 184 + 2: punctuation.separator.sequence.ssh.common 185 + 3: constant.other.range.ssh.common 186 + ip-addresses-with-cidr: 187 + - include: ipv6-with-cidr 188 + - include: ipv4-with-cidr 189 + port-numbers: 190 + - match: \b{{zero_to_65535}}(?![\w:]) 191 + scope: meta.number.integer.decimal.ssh.common constant.numeric.port-number.ssh.common 192 + match-all: 193 + - match: '\b(?xi: all )\b' 194 + scope: constant.language.boolean.true.ssh.common 195 + none: 196 + - match: \bnone\b 197 + scope: constant.language.null.ssh.common 198 + any: 199 + - match: \bany\b 200 + scope: constant.language.set.ssh.common 201 + boolean: 202 + - match: \byes\b 203 + scope: constant.language.boolean.true.ssh.common 204 + - match: \bno\b 205 + scope: constant.language.boolean.false.ssh.common 206 + boolean-with-typing: 207 + - include: boolean 208 + - match: \b(?:ye?|n)\b 209 + log-level: 210 + - match: '\b(?x: QUIET | FATAL | ERROR | INFO | DEBUG[1-3]? )\b' 211 + scope: constant.language.log-level.ssh.common 212 + possibly-quoted-value: 213 + - meta_content_scope: meta.mapping.value.sshd_config 214 + - match: '"' 215 + scope: punctuation.definition.string.begin.sshd_config 216 + push: 217 + - meta_scope: string.quoted.double.sshd_config 218 + - match: (")(?:\s*(\S.*))? 219 + captures: 220 + 1: punctuation.definition.string.end.sshd_config 221 + 2: invalid.illegal.sshd_config 222 + pop: 1 223 + - match: \n|$ 224 + scope: invalid.illegal.unclosed-string.sshd_config 225 + pop: 2 226 + - match: (?=\S) 227 + push: 228 + - meta_content_scope: string.unquoted.sshd_config 229 + - include: pop-before-nl 230 + - include: pop-nl 231 + string-patterns: 232 + - include: punctuation-comma-sequence 233 + - include: operator-exclamation 234 + - match: '"' 235 + scope: punctuation.definition.string.begin.ssh.common 236 + push: 237 + - meta_content_scope: string.quoted.double.ssh.common 238 + - match: '"' 239 + scope: punctuation.definition.string.end.ssh.common 240 + pop: 1 241 + - include: wildcards 242 + - match: (?=\S) 243 + push: 244 + - meta_content_scope: string.unquoted.ssh.common 245 + - match: (?=[,!\s]) 246 + pop: 1 247 + - include: wildcards 248 + paths: 249 + - match: (?=~?[\w.\-?*${}%]*/[\w.\-?*${}%]?) 250 + push: 251 + - meta_scope: meta.path.ssh.common entity.name.ssh.common 252 + - match: (?=[\s,"]) 253 + pop: 1 254 + - match: ~[\w\-.]* 255 + scope: variable.language.home.ssh.common 256 + - match: (/)(?:(\.{1,2})(?=/)|\.(?!/))? 257 + captures: 258 + 1: punctuation.separator.path.ssh.common 259 + 2: constant.other.placeholder.ssh.common 260 + - match: \.(?=[\w*?%]) 261 + scope: punctuation.separator.sequence.ssh.common 262 + - include: wildcards 263 + - include: tokens 264 + - include: environment-variables 265 + none-command-values: 266 + - match: \s*(none)\b[ \t]*$ 267 + captures: 268 + 1: constant.language.null.ssh.common 269 + - match: \s*((")(none)("))[ \t]*$ 270 + captures: 271 + 1: string.quoted.double.ssh.common 272 + 2: punctuation.definition.string.begin.ssh.common 273 + 3: constant.language.null.ssh.common 274 + 4: punctuation.definition.string.end.ssh.common 275 + tokens: 276 + - match: '%%' 277 + scope: constant.character.escape.sshd_config 278 + - match: '%[hUu]' 279 + scope: constant.other.placeholder.sshd_config 280 + environment-variables: [] 281 + pop-nl: 282 + - match: \n 283 + pop: 1 284 + pop-before-nl: 285 + - match: (?=\n) 286 + pop: 1 287 + ssh-ciphers: 288 + - match: \b(?:twofish256\-gcm@libassh\.org|twofish256\-ctr|twofish192\-ctr|twofish128\-gcm@libassh\.org|twofish128\-ctr|twofish\-ctr|crypticore128@ssh\.com|chacha20\-poly1305@openssh\.com|chacha20\-poly1305|camellia256\-ctr@openssh\.org|camellia256\-ctr|camellia192\-ctr@openssh\.org|camellia192\-ctr|camellia128\-ctr@openssh\.org|camellia128\-ctr|aes256\-gcm@openssh\.com|aes256\-gcm|aes256\-ctr|aes192\-gcm@openssh\.com|aes192\-ctr|aes128\-gcm@openssh\.com|aes128\-gcm|aes128\-ctr|AEAD_CAMELLIA_256_GCM|AEAD_CAMELLIA_128_GCM|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 289 + scope: support.function.cipher.ssh.crypto 290 + - match: \b(?:twofish256\-cbc|twofish192\-cbc|twofish128\-cbc|twofish\-ofb|twofish\-ecb|twofish\-cfb|twofish\-cbc|serpent256\-gcm@libassh\.org|serpent256\-ctr|serpent256\-cbc|serpent192\-ctr|serpent192\-cbc|serpent128\-gcm@libassh\.org|serpent128\-ctr|serpent128\-cbc|seed\-ctr@ssh\.com|seed\-cbc@ssh\.com|rijndael256\-cbc|rijndael192\-cbc|rijndael128\-cbc|rijndael\-cbc@ssh\.com|rijndael\-cbc@lysator\.liu\.se|none|idea\-ofb|idea\-ecb|idea\-ctr|idea\-cfb|idea\-cbc|grasshopper\-ctr128|des\-ofb|des\-ecb|des\-cfb|des\-cbc@ssh\.com|des\-cbc\-ssh1|des\-cbc|des|cast128\-ofb|cast128\-ecb|cast128\-ctr|cast128\-cfb|cast128\-cbc|cast128\-12\-ofb|cast128\-12\-ecb|cast128\-12\-ctr|cast128\-12\-cfb|cast128\-12\-cbc|camellia256\-cbc@openssh\.org|camellia256\-cbc|camellia192\-cbc@openssh\.org|camellia192\-cbc|camellia128\-cbc@openssh\.org|camellia128\-cbc|blowfish\-ecb|blowfish\-ctr|blowfish\-cfb|blowfish\-cbc|blowfish|arcfour256|arcfour128|arcfour|aes256\-cbc|aes192\-cbc|aes128\-ocb@libassh\.org|aes128\-cbc|3des\-ofb|3des\-ecb|3des\-ctr|3des\-cfb|3des\-cbc|3des)(?=[,\s\"]) 291 + scope: invalid.deprecated.cipher.ssh.crypto 292 + ssh-kex-algorithms: 293 + - match: \b(?:x25519\-kyber512\-sha512@aws\.amazon\.com|x25519\-kyber\-512r3\-sha256\-d00@amazon\.com|sntrup761x25519\-sha512@openssh\.com|sntrup4591761x25519\-sha512@tinyssh\.org|sm2kep\-sha2\-nistp256|rsa2048\-sha256|mlkem768x25519\-sha256|mlkem768nistp256\-sha256|mlkem1024nistp384\-sha384|m511\-sha512@libassh\.org|m383\-sha384@libassh\.org|kexguess2@matt\.ucc\.asn\.au|kexAlgoECDH521|kexAlgoECDH384|kexAlgoECDH256|kexAlgoCurve25519SHA256|kex\-strict\-s\-v00@openssh\.com|kex\-strict\-c\-v00@openssh\.com|gss\-nistp521\-sha512\-|gss\-nistp384\-sha384\-|gss\-nistp384\-sha256\-|gss\-nistp256\-sha256\-|gss\-group18\-sha512\-|gss\-group17\-sha512\-|gss\-group16\-sha512\-|gss\-group15\-sha512\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group15\-sha512\-|gss\-group14\-sha256\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha256\-|gss\-gex\-sha256\-|gss\-curve448\-sha512\-|gss\-curve25519\-sha256\-|gss\-13\.3\.132\.0\.10\-sha256\-|ext\-info\-s|ext\-info\-c|ecmqv\-sha2|ecdh\-sha2\-wiRIU8TKjMZ418sMqlqtvQ==|ecdh\-sha2\-qcFQaMAMGhTziMT0z\+Tuzw==|ecdh\-sha2\-nistt571|ecdh\-sha2\-nistp521|ecdh\-sha2\-nistp384|ecdh\-sha2\-nistp256|ecdh\-sha2\-nistp224|ecdh\-sha2\-nistp192|ecdh\-sha2\-nistk409|ecdh\-sha2\-nistk283|ecdh\-sha2\-nistb409|ecdh\-sha2\-mNVwCXAoS1HGmHpLvBC94w==|ecdh\-sha2\-m/FtSAmrV4j/Wy6RVUaK7A==|ecdh\-sha2\-h/SsxnLCtRBh7I9ATyeB3A==|ecdh\-sha2\-curve25519|ecdh\-sha2\-brainpoolp521r1@genua\.de|ecdh\-sha2\-brainpoolp384r1@genua\.de|ecdh\-sha2\-brainpoolp256r1@genua\.de|ecdh\-sha2\-D3FefCjYoJ/kfXgAyLddYA==|ecdh\-sha2\-9UzNcgwTlEnSCECZa7V1mw==|ecdh\-sha2\-1\.3\.132\.0\.38|ecdh\-sha2\-1\.3\.132\.0\.37|ecdh\-sha2\-1\.3\.132\.0\.36|ecdh\-sha2\-1\.3\.132\.0\.35|ecdh\-sha2\-1\.3\.132\.0\.34|ecdh\-sha2\-1\.3\.132\.0\.16|ecdh\-sha2\-1\.3\.132\.0\.10|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.7|ecdh\-nistp521\-kyber\-1024r3\-sha512\-d00@openquantumsafe\.org|ecdh\-nistp384\-kyber\-768r3\-sha384\-d00@openquantumsafe\.org|ecdh\-nistp256\-kyber\-512r3\-sha256\-d00@openquantumsafe\.org|diffie\-hellman_group17\-sha512|diffie\-hellman\-group18\-sha512@ssh\.com|diffie\-hellman\-group18\-sha512|diffie\-hellman\-group17\-sha512|diffie\-hellman\-group16\-sha512@ssh\.com|diffie\-hellman\-group16\-sha512|diffie\-hellman\-group16\-sha384@ssh\.com|diffie\-hellman\-group16\-sha256|diffie\-hellman\-group15\-sha512|diffie\-hellman\-group15\-sha384@ssh\.com|diffie\-hellman\-group15\-sha256@ssh\.com|diffie\-hellman\-group15\-sha256|diffie\-hellman\-group14\-sha256@ssh\.com|diffie\-hellman\-group14\-sha256|diffie\-hellman\-group14\-sha224@ssh\.com|diffie\-hellman\-group1\-sha256|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha512@ssh\.com|diffie\-hellman\-group\-exchange\-sha384@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256@ssh\.com|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha256|diffie\-hellman\-group\-exchange\-sha224@ssh\.com|curve448\-sha512@libssh\.org|curve448\-sha512|curve25519\-sha256@libssh\.org|curve25519\-sha256|Curve25519SHA256)(?=[,\s\"]) 294 + scope: support.function.kex-algorithm.ssh.crypto 295 + - match: \b(?:rsa1024\-sha1|kexAlgoDH1SHA1|kexAlgoDH14SHA1|gss\-group14\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group14\-sha1\-|gss\-group1\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-group1\-sha1\-|gss\-gex\-sha1\-toWM5Slw5Ew8Mqkay\+al2g==|gss\-gex\-sha1\-|ecdh\-sha2\-zD/b3hu/71952ArpUG4OjQ==|ecdh\-sha2\-qCbG5Cn/jjsZ7nBeR7EnOA==|ecdh\-sha2\-nistk233|ecdh\-sha2\-nistk163|ecdh\-sha2\-nistb233|ecdh\-sha2\-VqBg4QRPjxx1EXZdV0GdWQ==|ecdh\-sha2\-5pPrSUQtIaTjUSt5VZNBjg==|ecdh\-sha2\-4MHB\+NBt3AlaSRQ7MnB4cg==|ecdh\-sha2\-1\.3\.132\.0\.33|ecdh\-sha2\-1\.3\.132\.0\.27|ecdh\-sha2\-1\.3\.132\.0\.26|ecdh\-sha2\-1\.3\.132\.0\.1|ecdh\-sha2\-1\.2\.840\.10045\.3\.1\.1|diffie\-hellman\-group14\-sha1|diffie\-hellman\-group1\-sha1|diffie\-hellman\-group\-exchange\-sha1)(?=[,\s\"]) 296 + scope: invalid.deprecated.kex-algorithm.ssh.crypto 297 + ssh-key-types: 298 + - match: \b(?:x509v3\-sign\-rsa\-sha512@ssh\.com|x509v3\-sign\-rsa\-sha384@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256@ssh\.com|x509v3\-sign\-rsa\-sha256|x509v3\-sign\-rsa\-sha224@ssh\.com|x509v3\-sign\-dss\-sha512@ssh\.com|x509v3\-sign\-dss\-sha384@ssh\.com|x509v3\-sign\-dss\-sha256@ssh\.com|x509v3\-sign\-dss\-sha224@ssh\.com|x509v3\-rsa2048\-sha256|x509v3\-ecdsa\-sha2\-nistp521|x509v3\-ecdsa\-sha2\-nistp384|x509v3\-ecdsa\-sha2\-nistp256|x509v3\-ecdsa\-sha2\-1\.3\.132\.0\.10|webauthn\-sk\-ecdsa\-sha2\-nistp256@openssh\.com|ssh\-rsa\-sha512@ssh\.com|ssh\-rsa\-sha384@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha256@ssh\.com|ssh\-rsa\-sha2\-512|ssh\-rsa\-sha2\-256|ssh\-rsa|ssh\-gost\-2012\-512|ssh\-gost\-2012\-256|ssh\-gost\-2001|ssh\-ed448|ssh\-ed25519\-cert\-v01@openssh\.com|ssh\-ed25519|spi\-sign\-rsa|sk\-ecdsa\-sha2\-nistp256@openssh\.com|sk\-ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|rsa\-sha2\-512\-cert\-v01@openssh\.com|rsa\-sha2\-512|rsa\-sha2\-256\-cert\-v01@openssh\.com|rsa\-sha2\-256|eddsa\-e521\-shake256@libassh\.org|eddsa\-e382\-shake256@libassh\.org|ecdsa\-sha2\-nistt571|ecdsa\-sha2\-nistp521\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp521|ecdsa\-sha2\-nistp384\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp384|ecdsa\-sha2\-nistp256\-cert\-v01@openssh\.com|ecdsa\-sha2\-nistp256|ecdsa\-sha2\-nistk409|ecdsa\-sha2\-nistk283|ecdsa\-sha2\-nistk233|ecdsa\-sha2\-nistk163|ecdsa\-sha2\-nistb409|ecdsa\-sha2\-curve25519|ecdsa\-sha2\-1\.3\.132\.0\.10\-cert\-v01@openssh\.com|ecdsa\-sha2\-1\.3\.132\.0\.10|dsa3072\-sha256@libassh\.org|dsa2048\-sha256@libassh\.org|dsa2048\-sha224@libassh\.org)(?=[,\s\"]) 299 + scope: support.type.key-type.ssh.crypto 300 + - match: \b(?:x509v3\-ssh\-rsa|x509v3\-ssh\-dss|x509v3\-sign\-rsa\-sha1|x509v3\-sign\-rsa|x509v3\-sign\-dss\-sha1|x509v3\-sign\-dss|ssh\-xmss@openssh\.com|ssh\-xmss\-cert\-v01@openssh\.com|ssh\-rsa1|ssh\-rsa\-cert\-v01@openssh\.com|ssh\-rsa\-cert\-v00@openssh\.com|ssh\-dss\-sha512@ssh\.com|ssh\-dss\-sha384@ssh\.com|ssh\-dss\-sha256@ssh\.com|ssh\-dss\-sha224@ssh\.com|ssh\-dss\-cert\-v01@openssh\.com|ssh\-dss\-cert\-v00@openssh\.com|ssh\-dss|ssh\-dsa|spki\-sign\-rsa|spki\-sign\-dss|pgp\-sign\-rsa|pgp\-sign\-dss|null|ecdsa\-sha2\-nistp224|ecdsa\-sha2\-nistp192|ecdsa\-sha2\-nistb233)(?=[,\s\"]) 301 + scope: invalid.deprecated.key-type.ssh.crypto 302 + ssh-mac-algorithms: 303 + - match: \b(?:umac\-96@openssh\.com|umac\-64@openssh\.com|umac\-64\-etm@openssh\.com|umac\-32@openssh\.com|umac\-128@openssh\.com|umac\-128\-etm@openssh\.com|umac\-128|hmac\-sha512@ssh\.com|hmac\-sha512|hmac\-sha3\-512|hmac\-sha3\-384|hmac\-sha3\-256|hmac\-sha3\-224|hmac\-sha256@ssh\.com|hmac\-sha256\-96@ssh\.com|hmac\-sha256|hmac\-sha2\-56|hmac\-sha2\-512\-etm@openssh\.com|hmac\-sha2\-512\-96\-etm@openssh\.com|hmac\-sha2\-512|hmac\-sha2\-384|hmac\-sha2\-256\-etm@openssh\.com|hmac\-sha2\-256\-96\-etm@openssh\.com|hmac\-sha2\-256|hmac\-sha2\-224|crypticore\-mac@ssh\.com|chacha20\-poly1305@openssh\.com|cbcmac\-twofish|cbcmac\-aes|aes256\-gcm|aes128\-gcm|AEAD_AES_256_GCM|AEAD_AES_128_GCM)(?=[,\s\"]) 304 + scope: support.function.mac-algorithm.ssh.crypto 305 + - match: \b(?:sha1\-8|sha1|ripemd160\-8|ripemd160|none|md5\-8|md5|hmac\-sha2\-512\-96|hmac\-sha2\-256\-96|hmac\-sha1\-etm@openssh\.com|hmac\-sha1\-96\-etm@openssh\.com|hmac\-sha1\-96|hmac\-sha1|hmac\-ripemd160@openssh\.com|hmac\-ripemd160\-etm@openssh\.com|hmac\-ripemd160\-96|hmac\-ripemd160|hmac\-ripemd|hmac\-md5\-etm@openssh\.com|hmac\-md5\-96\-etm@openssh\.com|hmac\-md5\-96|hmac\-md5|cbcmac\-rijndael|cbcmac\-des|cbcmac\-blowfish|cbcmac\-3des)(?=[,\s\"]) 306 + scope: invalid.deprecated.mac-algorithm.ssh.crypto 307 + parameters: 308 + - include: comments 309 + - include: parameter-forcecommand 310 + - include: parameter-authorizedkeyscommand 311 + - include: parameter-authorizedprincipalscommand 312 + - include: parameter-path-with-tokens 313 + - include: parameter-routingdomain 314 + - include: parameter-with-boolean-values 315 + - include: parameter-generic 316 + pop-before-match-option: 317 + - include: pop-before-nl 318 + - match: '(?=\s*(?xi: all | user | group | host | (?:local)? address | localport 319 + )\b)' 320 + pop: 1 321 + pop-before-next-match: 322 + - match: (?=^\s*(?i:Match)\b) 323 + pop: 1 324 + match: 325 + - match: ^\s*((?i:Match))\b 326 + captures: 327 + 1: keyword.control.conditional.sshd_config 328 + set: match-conditions 329 + match-conditions: 330 + - meta_scope: meta.block.match.sshd_config 331 + - meta_content_scope: meta.statement.conditional.sshd_config 332 + - match: \n 333 + set: match-body 334 + - include: operator-exclamation 335 + - include: match-all 336 + - match: '\b(?xi: invalid-user )\b' 337 + scope: constant.language.null.sshd_config 338 + - match: '\b(?xi: host )\b' 339 + scope: meta.mapping.key.sshd_config keyword.other.sshd_config 340 + with_prototype: 341 + - include: punctuation-dot-sequence 342 + push: 343 + - meta_content_scope: meta.mapping.value.sshd_config 344 + - include: pop-before-match-option 345 + - include: string-patterns 346 + - match: '\b(?xi: user | group )\b' 347 + scope: meta.mapping.key.sshd_config keyword.other.sshd_config 348 + push: 349 + - meta_content_scope: meta.mapping.value.sshd_config 350 + - include: pop-before-match-option 351 + - include: string-patterns 352 + - match: '\b(?xi: (?:local)? address )\b' 353 + scope: meta.mapping.key.sshd_config keyword.other.sshd_config 354 + push: 355 + - meta_content_scope: meta.mapping.value.sshd_config 356 + - include: pop-before-match-option 357 + - include: operator-exclamation 358 + - include: wildcards 359 + - include: punctuation-comma-sequence 360 + - include: ip-addresses-with-cidr 361 + - match: '\b(?xi: localport )\b' 362 + scope: meta.mapping.key.sshd_config keyword.other.sshd_config 363 + push: 364 + - meta_content_scope: meta.mapping.value.sshd_config 365 + - include: pop-before-match-option 366 + - include: port-numbers 367 + - match: '\b(?xi: rdomain )\b' 368 + scope: meta.mapping.key.sshd_config keyword.other.sshd_config 369 + push: 370 + - meta_content_scope: meta.mapping.value.sshd_config 371 + - include: pop-before-match-option 372 + - match: \b{{zero_to_255}}\b 373 + scope: meta.number.integer.decimal.sshd_config constant.numeric.value.sshd_config 374 + match-body: 375 + - meta_content_scope: meta.block.match.sshd_config 376 + - include: pop-before-next-match 377 + - include: parameters 378 + parameter-forcecommand: 379 + - match: ^\s*((?i:ForceCommand))\b\s*(=)? 380 + captures: 381 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 382 + 2: keyword.operator.assignment.sshd_config 383 + push: 384 + - meta_content_scope: meta.mapping.value.sshd_config 385 + - include: pop-nl 386 + - include: none-command-values 387 + - match: '"' 388 + scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config 389 + escape: (")|(?=$) 390 + escape_captures: 391 + 1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config 392 + embed_scope: string.quoted.double.sshd_config 393 + embed: scope:source.shell 394 + - match: (?=\S) 395 + escape: (?=$) 396 + embed: scope:source.shell 397 + parameter-authorizedkeyscommand: 398 + - match: ^\s*((?i:AuthorizedKeysCommand))\b\s*(=)? 399 + captures: 400 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 401 + 2: keyword.operator.assignment.sshd_config 402 + push: 403 + - meta_content_scope: meta.mapping.value.sshd_config 404 + - include: pop-nl 405 + - match: '"' 406 + scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config 407 + escape: (")|(?=$) 408 + escape_captures: 409 + 1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config 410 + embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.authorizedkeyscommand 411 + embed: scope:source.shell.embedded.ssh.authorizedkeyscommand 412 + - match: (?=\S) 413 + escape: (?=$) 414 + embed: scope:source.shell.embedded.ssh.authorizedkeyscommand 415 + parameter-authorizedprincipalscommand: 416 + - match: ^\s*((?i:AuthorizedPrincipalsCommand))\b\s*(=)? 417 + captures: 418 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 419 + 2: keyword.operator.assignment.sshd_config 420 + push: 421 + - meta_content_scope: meta.mapping.value.sshd_config 422 + - include: pop-nl 423 + - match: '"' 424 + scope: string.quoted.double.sshd_config punctuation.definition.string.begin.sshd_config 425 + escape: (")|(?=$) 426 + escape_captures: 427 + 1: meta.mapping.value.sshd_config string.quoted.double.sshd_config punctuation.definition.string.end.sshd_config 428 + embed_scope: string.quoted.double.ssh_config source.shell.embedded.ssh.authorizedprincipalscommand 429 + embed: scope:source.shell.embedded.ssh.authorizedprincipalscommand 430 + - match: (?=\S) 431 + escape: (?=$) 432 + embed: scope:source.shell.embedded.ssh.authorizedprincipalscommand 433 + parameter-path-with-tokens: 434 + - match: '^\s*((?ix: AuthorizedKeysFile | AuthorizedPrincipalsFile | ChrootDirectory 435 + ))\b\s*(=)?' 436 + captures: 437 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 438 + 2: keyword.operator.assignment.sshd_config 439 + with_prototype: 440 + - include: tokens 441 + - include: none 442 + - include: paths 443 + push: possibly-quoted-value 444 + parameter-routingdomain: 445 + - match: ^\s*((?i:RoutingDomain))\b\s*(=)? 446 + captures: 447 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 448 + 2: keyword.operator.assignment.sshd_config 449 + with_prototype: 450 + - match: '%D' 451 + scope: constant.other.placeholder.sshd_config 452 + - include: numeric-values 453 + push: possibly-quoted-value 454 + parameter-with-boolean-values: 455 + - match: ^\s*({{parameters_boolean}})\s*(=)? 456 + captures: 457 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 458 + 2: keyword.operator.assignment.sshd_config 459 + with_prototype: 460 + - include: boolean-with-typing 461 + - match: '[^"\s]+' 462 + scope: invalid.illegal.sshd_config 463 + push: possibly-quoted-value 464 + parameter-generic: 465 + - match: ^\s*([a-zA-Z1]+)\b\s*(=)? 466 + captures: 467 + 1: meta.mapping.key.sshd_config keyword.other.sshd_config 468 + 2: keyword.operator.assignment.sshd_config 469 + with_prototype: 470 + - include: generic-parameter-values 471 + push: possibly-quoted-value 472 + generic-parameter-values: 473 + - include: boolean 474 + - include: none 475 + - include: any 476 + - match: '\b(?xi: default )\b' 477 + scope: constant.language.default.sshd_config 478 + - include: ssh-key-types 479 + - include: ssh-ciphers 480 + - include: ssh-kex-algorithms 481 + - include: ssh-mac-algorithms 482 + - include: ipv6-square-bracket 483 + - include: ip-addresses-with-cidr 484 + - include: time-values 485 + - include: bytes-values 486 + - include: operator-exclamation 487 + - include: wildcards 488 + - include: punctuation-comma-sequence 489 + - include: log-level 490 + - include: paths 491 + - include: numeric-values 492 + - match: ':' 493 + scope: punctuation.separator.sequence.sshd_config 494 + numeric-values: 495 + - match: \b\d+(?=[\s,:"]) 496 + scope: constant.numeric.sshd_config
+28 -22
templates/404.html
··· 1 {% extends "base.html" %} {% block content %} 2 3 <div 4 - style=" 5 - display: flex; 6 - flex-direction: column; 7 - justify-content: center; 8 - align-items: center; /* Center vertically */ 9 - height: 100%; /* Adjust height as needed */ 10 - " 11 > 12 - <p><strong>I think you stumbled on something non existent :)</strong></p> 13 - <p><i id="redirect">Redirecting you back home in 5</i></p> 14 </div> 15 16 - <script> 17 - const link = document.getElementById("redirect"); 18 19 - // count down to redirect 20 - let count = 5; 21 - const interval = setInterval(() => { 22 - count--; 23 - link.innerText = `Redirecting you back home in ${count}`; 24 - if (count === 0) { 25 - clearInterval(interval); 26 - window.location.href = "/"; 27 - } 28 - }, 1000); 29 - </script> 30 31 {% endblock content %}
··· 1 {% extends "base.html" %} {% block content %} 2 3 <div 4 + id="suggestions" 5 + style=" 6 + display: flex; 7 + flex-direction: column; 8 + justify-content: center; 9 + align-items: center; /* Center vertically */ 10 + height: 100%; /* Adjust height as needed */ 11 + " 12 > 13 + <p><strong>I think you stumbled on something non existent :)</strong></p> 14 </div> 15 16 + {% set jsHash = get_hash(path="js/404-matcher.js", sha_type=256, base64=true) %} 17 + <script 18 + src="{{ get_url(path='js/404-matcher.js?' ~ jsHash, trailing_slash=false) | safe }}" 19 + defer 20 + ></script> 21 22 + <!-- <script> 23 + const link = document.getElementById("redirect"); 24 + 25 + // count down to redirect 26 + let count = 5; 27 + const interval = setInterval(() => { 28 + count--; 29 + link.innerText = `Redirecting you back home in ${count}`; 30 + if (count === 0) { 31 + clearInterval(interval); 32 + window.location.href = "/"; 33 + } 34 + }, 1000); 35 + </script> --> 36 37 {% endblock content %}
+13 -18
templates/base.html
··· 1 - <!DOCTYPE html> 2 - <html lang="{% if page %}{{ page.lang }}{% else %}{{ config.default_language }}{% endif %}"> 3 - <head> 4 - {% include "head.html" %} 5 - </head> 6 - <body> 7 - <header> 8 - {% include "header.html" %} 9 - </header> 10 - <main> 11 - {% block content %} 12 - {% endblock content %} 13 - </main> 14 - <footer> 15 - {% include "footer.html" %} 16 - </footer> 17 - </body> 18 - </html>
··· 1 + <!doctype html> 2 + <html 3 + lang="{% if page %}{{ page.lang }}{% else %}{{ config.default_language }}{% endif %}" 4 + > 5 + <head> 6 + {% include "head.html" %} 7 + </head> 8 + <body> 9 + <header>{% include "header.html" %}</header> 10 + <main>{% block content %} {% endblock content %}</main> 11 + <footer>{% include "footer.html" %}</footer> 12 + </body> 13 + </html>
+40 -40
templates/blog-page.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 <div><a href="..">..</a>/<span class="accent-data">{{ page.slug }}</span></div> 5 - <time datetime="{{ page.date }}">Published on: <span class="accent-data">{{ page.date }}</span></time> 6 - {% if config.extra.author and config.extra.display_author == true %} 7 - <address rel="author">By <span class="accent-data">{{config.extra.author}}</span></address> 8 - {% endif %} 9 - <h1>{{ page.title }}</h1> 10 11 - {% if page.toc and page.extra.toc %} 12 - <h2>Table of contents</h2> 13 - <ul> 14 - {% for h1 in page.toc %} 15 - <li> 16 - <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> 17 - {% if h1.children %} 18 <ul> 19 - {% for h2 in h1.children %} 20 - <li> 21 - <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> 22 - <ul> 23 - {% for h3 in h2.children %} 24 - <li> 25 - <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a> 26 - </li> 27 - {% endfor %} 28 - </ul> 29 - </li> 30 - {% endfor %} 31 </ul> 32 {% endif %} 33 - </li> 34 - {% endfor %} 35 - </ul> 36 - {% endif %} 37 38 - {{ page.content | safe }} 39 40 - <p class="tags-data"> 41 - {% if page.taxonomies.tags %} 42 - {% for tag in page.taxonomies.tags %} 43 - <a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a> 44 - {% endfor %} 45 - {% endif %} 46 - </p> 47 - {% endblock content %}
··· 1 + {% extends "base.html" %} {% block content %} 2 <div><a href="..">..</a>/<span class="accent-data">{{ page.slug }}</span></div> 3 + <article class="h-entry"> 4 + <a class="u-url" href="{{ page.permalink }}" style="display: none"> </a> 5 + <span class="dt-published">Published <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D" class="accent-data">{{ page.date | split(pat="T") | first }}</relative-time></span> 6 + {% if config.extra.author and config.extra.display_author == true %} 7 + <address rel="author"> 8 + By 9 + <a 10 + rel="author" 11 + class="accent-data p-author h-card text-glow" 12 + href="https://dunkirk.sh" 13 + >{{config.extra.author}}</a 14 + > 15 + </address> 16 + {% endif %} 17 + <h1>{{ page.title }}</h1> 18 19 + {% if page.toc and page.extra["has_toc"] %} 20 + <h2>Table of contents</h2> 21 <ul> 22 + {% for h2 in page.toc %} 23 + <li> 24 + <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> 25 + <ul> 26 + {% for h3 in h2.children %} 27 + <li> 28 + <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a> 29 + </li> 30 + {% endfor %} 31 + </ul> 32 + </li> 33 + {% endfor %} 34 </ul> 35 {% endif %} 36 37 + <div class="e-content p-name">{{ page.content | safe }}</div> 38 39 + <p class="tags-data"> 40 + {% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %} 41 + <a href="/tags/{{ tag | slugify }}" class="p-category text-glow" 42 + >|{{ tag }}|</a 43 + > 44 + {% endfor %} {% endif %} 45 + </p> 46 + </article> 47 + {% endblock content %}
+25 -13
templates/blog.html
··· 1 {% extends "base.html" %} {% block content %} 2 <h1 class="title">{{ section.title }}</h1> 3 4 - <p role="heading">--- <span style="letter-spacing: 0.1em;">Main Blog</span> ---</p> 5 6 <ul> 7 - <!-- If you are using pagination, section.pages will be empty. 8 You need to use the paginator object --> 9 - {% for page in section.pages %} 10 - {% if "archival" not in page.taxonomies.tags %} 11 - <li>{{ page.date }} &mdash; <a href="{{ page.permalink | safe }}">{{ page.title }}</a></li> 12 - {% endif %} 13 - {% endfor %} 14 </ul> 15 16 - <p role="heading" >--- <span style="letter-spacing: 0.213em;">Archival</span> ---</p> 17 18 <ul> 19 - {% for page in section.pages %} 20 - {% if "archival" in page.taxonomies.tags %} 21 - <li>{{ page.date }} &mdash; <a href="{{ page.permalink | safe }}">{{ page.title }}</a> (archival)</li> 22 - {% endif %} 23 - {% endfor %} 24 </ul> 25 {% endblock content %}
··· 1 {% extends "base.html" %} {% block content %} 2 <h1 class="title">{{ section.title }}</h1> 3 4 + <p role="heading"> 5 + --- <span style="letter-spacing: 0.1em">Main Blog</span> --- 6 + </p> 7 8 <ul> 9 + <!-- If you are using pagination, section.pages will be empty. 10 You need to use the paginator object --> 11 + {% for page in section.pages %} {% if "archival" not in page.taxonomies.tags 12 + %} 13 + <li> 14 + <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> &mdash; 15 + <a href="{{ page.permalink | safe }}" class="text-glow" 16 + >{{ page.title }}</a 17 + > 18 + </li> 19 + {% endif %} {% endfor %} 20 </ul> 21 22 + <p role="heading"> 23 + --- <span style="letter-spacing: 0.213em">Archival</span> --- 24 + </p> 25 26 <ul> 27 + {% for page in section.pages %} {% if "archival" in page.taxonomies.tags %} 28 + <li> 29 + <relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> &mdash; 30 + <a href="{{ page.permalink | safe }}" class="text-glow" 31 + >{{ page.title }}</a 32 + > 33 + (archival) 34 + </li> 35 + {% endif %} {% endfor %} 36 </ul> 37 {% endblock content %}
+55 -21
templates/footer.html
··· 1 <hr /> 2 <div id="footer-container"> 3 - <div> 4 - <p>&copy; {{ now() | date(format="%Y") }} Kieran Klukas</p> 5 - <p> 6 - Content licensed under 7 - <a 8 - target="_blank" 9 - rel="noopener noreferrer" 10 - href="https://creativecommons.org/licenses/by-nc-sa/4.0/" 11 - >CC BY-NC-SA 4.0</a 12 - > 13 - </p> 14 - <p> 15 - Code licensed under 16 - <a 17 - target="_blank" 18 - rel="noopener noreferrer" 19 - href="https://github.com/kcoderhtml/zera/blob/master/LICENSE.md" 20 - >AGPL 3.0</a 21 - > 22 - </p> 23 - </div> 24 </div>
··· 1 <hr /> 2 <div id="footer-container"> 3 + <p class="badge-row"> 4 + <a href="https://512kb.club"><img src="/badges/green-team.gif" 5 + alt="a proud member of the green team of 512KB club" /></a> 6 + <a href="https://hackclub.com"><img src="/badges/hackclub.png" alt="linux powered" /></a> 7 + <a href="https://dunkirk.sh/ai"><img src="/badges/MadeByAHuman_04.svg" alt="made by a human" /></a> 8 + <a href="https://tangled.org"><img src="/badges/tangled.png" alt="tangled beta" /></a> 9 + <a href="https://kagi.com/smallweb"><img src="/badges/kagi.gif" alt="kagi smallweb" /></a> 10 + <a href="https://tangled.org/@dunkirk.sh/dots"><img src="/badges/powered-by-nix.gif" alt="powered by nix" /></a> 11 + <a href="https://tangled.org/@dunkirk.sh/nixvim"><img src="/badges/made-with-neovim.png" /></a> 12 + </p> 13 + <p style="margin-bottom: 0.5rem"> 14 + &copy; {{ now() | date(format="%Y") }} Kieran Klukas || 15 + <code id="visits">0</code> page visits || {% set hash = 16 + get_env(name="CF_PAGES_COMMIT_SHA", default=load_data(path=".git/refs/heads/main", required=false))%}{% if hash is 17 + not string %}{% set hash = "unknown" %}{% endif %}<a href=https://tangled.sh/@dunkirk.sh/zera/commit/{{ hash 18 + }}>zera@{{ hash | 19 + truncate(length=7, end="")}}</a> 20 + </p> 21 + <p style="margin-bottom: 0.5rem"> 22 + Webrings: 23 + <a href="https://w.elr.sh">elr</a> 24 + [<a href='javascript:void(0)' onclick='randomSite()'>random</a> | 25 + <a href='#' id='prev-link'>prev</a> | 26 + <a href='#' id='next-link'>next</a>] โ€ข 27 + <a href="https://ctp-webr.ing">ctp</a> 28 + [<a href="https://ctp-webr.ing/dunkirk/previous">prev</a> | 29 + <a href="https://ctp-webr.ing/dunkirk/next">next</a>] 30 + </p> 31 + 32 + <script type="text/javascript" src="https://w.elr.sh/onionring-variables.js"></script> 33 + <script> 34 + thisSite = "https://dunkirk.sh" 35 + thisIndex = null; 36 + 37 + for (i = 0; i < sites.length; i++) { 38 + if (thisSite.startsWith(sites[i])) { 39 + thisIndex = i; 40 + break; 41 + } 42 + } 43 + 44 + function randomSite() { 45 + otherSites = sites.slice(); 46 + otherSites.splice(thisIndex, 1); 47 + randomIndex = Math.floor(Math.random() * otherSites.length); 48 + location.href = otherSites[randomIndex]; 49 + } 50 + 51 + 52 + previousIndex = (thisIndex - 1 < 0) ? sites.length - 1 : thisIndex - 1; 53 + nextIndex = (thisIndex + 1 >= sites.length) ? 0 : thisIndex + 1; 54 + 55 + document.getElementById('prev-link').href = sites[previousIndex]; 56 + document.getElementById('next-link').href = sites[nextIndex]; 57 + </script> 58 </div>
+106 -35
templates/head.html
··· 3 <meta content="text/html; charset=UTF-8" http-equiv="content-type" /> 4 <meta name="viewport" content="width=device-width, initial-scale=1" /> 5 <meta name="robots" content="index, follow" /> 6 {% if page.title %} {% set title = page.title %} {% elif section.title %} {% set 7 - title = section.title %} {% elif config.title %} {% set title = config.title %} 8 - {% endif %} {% if page.extra.author %} {% set author = page.extra.author %} {% 9 - elif section.extra.author %} {% set author = section.extra.author %} {% elif 10 config.extra.author %} {% set author = config.extra.author %} {% endif %} {% if 11 page.description %} {% set description = page.description | truncate(length=150) 12 %} {% elif section.description %} {% set description = section.description | 13 truncate(length=150) %} {% elif config.description %} {% set description = 14 config.description | truncate(length=150) %} {% endif %} {% if page.extra.image 15 - %} {% set image = get_url(path="og.png", trailing_slash=false) %} {% elif 16 - section.extra.image %} {% set image = get_url(path=section.extra.image, 17 trailing_slash=false) %} {% elif page.path %} {% set image = 18 - get_url(path=page.path ~ "og.png", trailing_slash=false) %} {% else %} {% set 19 - image = get_url(path="og.png", trailing_slash=false) %} {% endif %} {% if 20 - page.permalink %} {% set url = page.permalink %} {% elif section.permalink %} {% 21 - set url = section.permalink %} {% elif config.base_url %} {% set url = 22 - config.base_url %} {% endif %} {% if title %} 23 <title>{{ title }}</title> 24 {% endif %} {% block metatags %} {% if title %} 25 <meta name="title" content="{{ title }}" /> ··· 50 <meta property="twitter:image" content="{{ image }}" /> 51 {% endif %} {% endif %} 52 <link rel="canonical" href="{{ url | safe }}" /> 53 - {% if image %} 54 - <link 55 - rel="shortcut icon" 56 - type="image/x-icon" 57 - href="{{ get_url(path=config.extra.favicon, trailing_slash=false) }}" 58 - /> 59 - {% endif %} {% endblock metatags %} {% if config.generate_feeds %} {% block feed 60 - %} <link rel="alternate" type="application/atom+xml" title="RSS" href="{{ 61 - get_url(path="atom.xml", trailing_slash=false) }}"> {% endblock feed %} {% endif 62 - %} {% block css %} {% set cssHash = get_hash(path="css/main.css", sha_type=256, 63 base64=true) %} 64 - <link 65 - rel="stylesheet" 66 - type="text/css" 67 - href="{{ get_url(path='css/main.css?' ~ cssHash, trailing_slash=false) | safe }}" 68 - /> 69 - {% endblock css %} {% set jsHash = get_hash(path="js/script.js", sha_type=256, 70 base64=true) %} 71 - <script 72 - src="{{ get_url(path='js/script.js?' ~ jsHash, trailing_slash=false) | safe }}" 73 - defer 74 - ></script> 75 - <script 76 - defer 77 - data-domain="dunkirk.sh" 78 - src="https://nexus.kieranklukas.com/js/script.outbound-links.file-downloads.js" 79 - ></script>
··· 3 <meta content="text/html; charset=UTF-8" http-equiv="content-type" /> 4 <meta name="viewport" content="width=device-width, initial-scale=1" /> 5 <meta name="robots" content="index, follow" /> 6 + <link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" /> 7 {% if page.title %} {% set title = page.title %} {% elif section.title %} {% set 8 + title = section.title %} {% elif term %} {% set title = "|" ~ term.name ~ "|" %} 9 + {% elif current_path and "tags" in current_path %} {% set title = "Root Index" 10 + %} {% elif config.title %} {% set title = config.title %} {% endif %} {% if 11 + page.extra.author %} {% set author = page.extra.author %} {% elif 12 + section.extra.author %} {% set author = section.extra.author %} {% elif 13 config.extra.author %} {% set author = config.extra.author %} {% endif %} {% if 14 page.description %} {% set description = page.description | truncate(length=150) 15 %} {% elif section.description %} {% set description = section.description | 16 truncate(length=150) %} {% elif config.description %} {% set description = 17 config.description | truncate(length=150) %} {% endif %} {% if page.extra.image 18 + %} {% set image = get_url(path=page.extra.image, trailing_slash=false) %} {% 19 + elif section.extra.image %} {% set image = get_url(path=section.extra.image, 20 trailing_slash=false) %} {% elif page.path %} {% set image = 21 + get_url(path=page.path ~ "og.png", trailing_slash=false) %} {% elif current_path 22 + %} {% set image = get_url(path=current_path ~ "og.png", trailing_slash=false) %} 23 + {% else %} {% set image = get_url(path="og.png", trailing_slash=false) %} {% 24 + endif %} {% if page.permalink %} {% set url = page.permalink %} {% elif 25 + section.permalink %} {% set url = section.permalink %} {% elif config.base_url 26 + %} {% set url = config.base_url %} {% endif %} {% if title %} {% if current_url 27 + and url != current_url %} {% set url = get_url(path=current_path, 28 + trailing_slash=true) %} {% endif %} 29 <title>{{ title }}</title> 30 {% endif %} {% block metatags %} {% if title %} 31 <meta name="title" content="{{ title }}" /> ··· 56 <meta property="twitter:image" content="{{ image }}" /> 57 {% endif %} {% endif %} 58 <link rel="canonical" href="{{ url | safe }}" /> 59 + <link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" /> 60 + <link rel="shortcut icon" href="/favicon/favicon.ico" /> 61 + <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" /> 62 + <meta name="apple-mobile-web-app-title" content="site@zera" /> 63 + <link rel="manifest" href="/favicon/site.webmanifest" /> 64 + {% endblock metatags %} {% if config.generate_feeds %} {% block 65 + feed%} 66 + <link rel="alternate" type="application/atom+xml" title="Kieran Klukas' Atom feed" href="{{ 67 + get_url(path=" atom.xml", trailing_slash=false) }}"> 68 + <link rel="alternate" type="application/rss+xml" title="Kieran Klukas' RSS feed" href="{{ get_url(path=" rss.xml", 69 + trailing_slash=false) }}"> {% endblock feed %} {% endif%} {% block css %} {% set 70 + cssHash = get_hash(path="css/main.css", sha_type=256, base64=true) %} 71 + <link rel="stylesheet" type="text/css" 72 + href="{{ get_url(path='css/main.css?' ~ cssHash, trailing_slash=false) | safe }}" /> 73 + {% endblock css %} 74 + 75 + {% set jsHash = get_hash(path="js/copy-button.js", sha_type=256, 76 + base64=true) %} 77 + <script src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}" defer></script> 78 + 79 + {% set emojiJsHash = get_hash(path="js/emoji-replace.js", sha_type=256, 80 + base64=true) %} 81 + <script src="{{ get_url(path='js/emoji-replace.js?' ~ emojiJsHash, trailing_slash=false) | safe }}" defer></script> 82 + 83 + {% set lightboxJsHash = get_hash(path="js/lightbox.js", sha_type=256, 84 base64=true) %} 85 + <script src="{{ get_url(path='js/lightbox.js?' ~ lightboxJsHash, trailing_slash=false) | safe }}" defer></script> 86 + 87 + {% set relativeTimeJsHash = get_hash(path="js/relative-time.js", sha_type=256, 88 base64=true) %} 89 + <script src="{{ get_url(path='js/relative-time.js?' ~ relativeTimeJsHash, trailing_slash=false) | safe }}" 90 + defer></script> 91 + 92 + <script type="speculationrules"> 93 + { 94 + "prerender": [ 95 + { 96 + "where": { 97 + "selector_matches": "a" 98 + } 99 + } 100 + ] 101 + } 102 + </script> 103 + 104 + <script> 105 + function cb(res) { 106 + const fmt = new Intl.NumberFormat('en', {notation: 'compact'}); 107 + const elements = document.querySelectorAll("[id='visits']"); 108 + elements.forEach(el => { 109 + el.innerText = fmt.format(res.value); 110 + el.title = res.value + " visits"; 111 + }); 112 + } 113 + </script> 114 + <script async 115 + src="https://abacus.jasoncameron.dev/hit/dunkirk.sh/counter{%- if url | split(pat='/') | slice(start=3) | join != '' -%}-{{url | split(pat='/') | slice(start=3) | join(sep=' ') | slugify}}{%- endif -%}?callback=cb"></script> 116 + 117 + <link rel="me" href="https://social.dino.icu/@taciturnaxoltol" /> 118 + <link rel="me" href="https://github.com/taciturnaxolotl" /> 119 + <link rel="me" href="https://bsky.app/profile/dunkirk.sh" /> 120 + <link rel="me" href="https://www.youtube.com/@kieran.rambles" /> 121 + <link rel="me" href="https://keyoxide.org/aspe:keyoxide.org:QMHCMT55EODYTEBQ5C7QOAFN6A" /> 122 + <link rel="me" href="https://serif.blue" /> 123 + <meta name="fediverse:creator" content="@taciturnaxoltol@social.dino.icu" /> 124 + 125 + <link rel="indieauth-metadata" href="https://indiko.dunkirk.sh/.well-known/oauth-authorization-server" /> 126 + <link rel="authorization_endpoint" href="https://indiko.dunkirk.sh/auth/authorize" /> 127 + <link rel="token_endpoint" href="https://indiko.dunkirk.sh/auth/token" /> 128 + <link rel="me" href="https://indiko.dunkirk.sh/u/tacy" /> 129 + 130 + <link rel="webmention" href="https://webmention.io/dunkirk.sh/webmention" /> 131 + 132 + <div class="h-card" style="display:none"> 133 + <a class="u-url" rel="me home" href="https://dunkirk.sh"> 134 + <span class="p-name">Kieran Klukas</span> 135 + </a> 136 + <p class="p-given-name">Kieran</p> 137 + <p class="p-family-name">Klukas</p> 138 + <p class="dt-bday">2008-04-27</p> 139 + <p class="p-sex">male</p> 140 + <p class="p-note"> 141 + {% set time = now() | date(format="%s") | int - 1209254400 %}{{ time / 31536000 | round(method="floor") }}, 142 + typescript nerd, videographer, frc programmer, semi retired fpv pilot 143 + </p> 144 + <a class="u-email" href="mailto:me@dunkirk.sh" rel="me">me@dunkirk.sh</a> 145 + <div class="p-adr h-adr"> 146 + <span class="p-country-name">United States of America</span> 147 + </div> 148 + <img class="u-photo" src="/pfps/fall.jpg" 149 + alt="kieran wearing a robotics sweatshirt and standing in front of a tree with fall leaves" /> 150 + </div>
+78 -27
templates/header.html
··· 1 - {% if config.extra.header_nav %} {% if not current_url %} {% set current_url = 2 - "" %} {% endif %} 3 - <nav id="nav-bar"> 4 - {% for nav_item in config.extra.header_nav %} 5 - <a 6 - href="{{ nav_item.url }}" 7 - class="{% if nav_item.url == current_url %}active{% endif %}" 8 - > 9 - {{ nav_item.name }} 10 - </a> 11 - {% endfor %} 12 - <div> 13 - <input type="checkbox" id="theme-toggle" style="display: none" /> 14 - <label for="theme-toggle" id="theme-toggle-label" 15 - ><svg id="theme-icon" class="icons"> 16 - <use 17 - href="{{ get_url(path='/icons.svg#lightMode', trailing_slash=false) | safe }}" 18 - ></use></svg 19 - ></label> 20 - <audio id="theme-sound"> 21 - <source 22 - src="{{ get_url(path='click.ogg', trailing_slash=false) | safe }}" 23 - type="audio/ogg" 24 - /> 25 - </audio> 26 - </div> 27 - </nav> 28 {% endif %}
··· 1 + {% if config.extra.header_nav %} 2 + {% if page %} 3 + {% set active_path = page.path | trim_end_matches(pat="/") %} 4 + {% elif section %} 5 + {% set active_path = section.path | trim_end_matches(pat="/") %} 6 + {% elif current_path %} 7 + {% set active_path = current_path | trim_end_matches(pat="/") %} 8 + {% else %} 9 + {% set active_path = "" %} 10 {% endif %} 11 + <div id="header-container"> 12 + <span id="now-playing"></span> 13 + <nav id="nav-bar"> 14 + {% for nav_item in config.extra.header_nav %} 15 + <a href="{{ nav_item.url }}" 16 + class="{% if nav_item.url == active_path or (nav_item.url == '/' and active_path == '') %}active{% endif %}"> 17 + {{ nav_item.name }} 18 + </a> 19 + {% endfor %} 20 + </nav> 21 + </div> 22 + <script> 23 + async function resolveDidToPds(did) { 24 + if (did.startsWith("did:plc:")) { 25 + const res = await fetch(`https://plc.directory/${did}`); 26 + const doc = await res.json(); 27 + return doc.service?.find(s => s.id === "#atproto_pds")?.serviceEndpoint; 28 + } else if (did.startsWith("did:web:")) { 29 + const domain = did.slice(8); 30 + const res = await fetch(`https://${domain}/.well-known/did.json`); 31 + const doc = await res.json(); 32 + return doc.service?.find(s => s.id === "#atproto_pds")?.serviceEndpoint; 33 + } 34 + return null; 35 + } 36 + async function fetchAtUriRecord(atUri) { 37 + const match = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/); 38 + if (!match) return null; 39 + const [, repo, collection, rkey] = match; 40 + const pds = await resolveDidToPds(repo); 41 + if (!pds) return null; 42 + const url = `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`; 43 + const res = await fetch(url); 44 + return res.ok ? res.json() : null; 45 + } 46 + let nowPlayingTimeout = null; 47 + function fetchNowPlaying() { 48 + if (nowPlayingTimeout) clearTimeout(nowPlayingTimeout); 49 + fetchAtUriRecord("at://did:plc:krxbvxvis5skq7jj6eot23ul/fm.teal.alpha.actor.status/self") 50 + .then((data) => { 51 + const el = document.getElementById("now-playing"); 52 + if (!data?.value?.item) { 53 + el.innerHTML = ""; 54 + nowPlayingTimeout = setTimeout(fetchNowPlaying, 60000); 55 + return; 56 + } 57 + const item = data.value.item; 58 + const expiry = parseInt(data.value.expiry, 10) * 1000; 59 + const now = Date.now(); 60 + if (now > expiry) { 61 + el.innerHTML = ""; 62 + nowPlayingTimeout = setTimeout(fetchNowPlaying, 60000); 63 + return; 64 + } 65 + el.innerHTML = ` 66 + <div class="now-playing-line"><span>๐ŸŽต</span><a href="${item.originUrl || '#'}" target="_blank" rel="noopener"><span class="track-name">${item.trackName}</span></a></div> 67 + <div class="now-playing-line artist-line">${item.artists?.[0]?.artistName || 'Unknown'}</div> 68 + <div class="now-playing-line"><relative-time datetime="${item.playedTime}" threshold="P1D"></relative-time></div> 69 + `; 70 + const timeUntilExpiry = expiry - now + 5000; 71 + nowPlayingTimeout = setTimeout(fetchNowPlaying, timeUntilExpiry); 72 + }) 73 + .catch(() => { 74 + nowPlayingTimeout = setTimeout(fetchNowPlaying, 60000); 75 + }); 76 + } 77 + document.addEventListener("DOMContentLoaded", fetchNowPlaying); 78 + </script> 79 + {% endif %}
+12 -12
templates/index.html
··· 3 {% else %} {% set pages = section.pages %} {% endif %} 4 5 <ul class="titleList"> 6 - {% for page in pages %} 7 - <li> 8 - <a href="{{ page.permalink | safe }}">{{ page.title }}</a> 9 - <br /> 10 - {{ page.description }} 11 - </li> 12 - {% endfor %} 13 </ul> 14 15 {% if paginator %} 16 <div class="metaData"> 17 - {% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp 18 - <a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{ 19 - paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if 20 - paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp 21 - <a href="{{ paginator.last }}">โฅธ</a>{% endif %} 22 </div> 23 {% endif %} {% endif %} {% endblock content %}
··· 3 {% else %} {% set pages = section.pages %} {% endif %} 4 5 <ul class="titleList"> 6 + {% for page in pages %} 7 + <li> 8 + <a href="{{ page.permalink | safe }}">{{ page.title }}</a> 9 + <br /> 10 + {{ page.description }} 11 + </li> 12 + {% endfor %} 13 </ul> 14 15 {% if paginator %} 16 <div class="metaData"> 17 + {% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp 18 + <a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{ 19 + paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if 20 + paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp 21 + <a href="{{ paginator.last }}">โฅธ</a>{% endif %} 22 </div> 23 {% endif %} {% endif %} {% endblock content %}
+22 -22
templates/page.html
··· 4 {% if page.toc and page.extra.toc %} 5 <h2>Table of contents</h2> 6 <ul> 7 - {% for h1 in page.toc %} 8 - <li> 9 - <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> 10 - {% if h1.children %} 11 - <ul> 12 - {% for h2 in h1.children %} 13 - <li> 14 - <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> 15 <ul> 16 - {% for h3 in h2.children %} 17 - <li> 18 - <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a> 19 - </li> 20 - {% endfor %} 21 </ul> 22 - </li> 23 - {% endfor %} 24 - </ul> 25 - {% endif %} 26 - </li> 27 - {% endfor %} 28 </ul> 29 {% endif %} {{ page.content | safe }} 30 31 <p class="tags-data"> 32 - {% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %} 33 - <a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a> 34 - {% endfor %} {% endif %} 35 </p> 36 {% endblock content %}
··· 4 {% if page.toc and page.extra.toc %} 5 <h2>Table of contents</h2> 6 <ul> 7 + {% for h1 in page.toc %} 8 + <li> 9 + <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> 10 + {% if h1.children %} 11 <ul> 12 + {% for h2 in h1.children %} 13 + <li> 14 + <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> 15 + <ul> 16 + {% for h3 in h2.children %} 17 + <li> 18 + <a href="{{ h3.permalink | safe }}">{{ h3.title }}</a> 19 + </li> 20 + {% endfor %} 21 + </ul> 22 + </li> 23 + {% endfor %} 24 </ul> 25 + {% endif %} 26 + </li> 27 + {% endfor %} 28 </ul> 29 {% endif %} {{ page.content | safe }} 30 31 <p class="tags-data"> 32 + {% if page.taxonomies.tags %} {% for tag in page.taxonomies.tags %} 33 + <a href="/tags/{{ tag | slugify }}">&#47;{{ tag }}&#47;</a> 34 + {% endfor %} {% endif %} 35 </p> 36 {% endblock content %}
+16 -18
templates/section.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 <h1>{{ section.title }}</h1> 5 6 - {{ section.content | safe }} 7 - 8 - {% if paginator %} 9 - {% set pages = paginator.pages %} 10 - {% else %} 11 - {% set pages = section.pages %} 12 - {% endif %} 13 14 <ul class="title-list"> 15 - {% for page in pages %} 16 - <li> 17 - <a href="{{ page.permalink | safe }}">{{ page.title }}</a> 18 - </li> 19 - {% endfor %} 20 </ul> 21 22 {% if paginator %} 23 - <div class="accent-data">{% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp <a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{ paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp <a href="{{ paginator.last }}">โฅธ</a>{% endif %}</div> 24 - {% endif %} 25 - {% endblock content %}
··· 1 + {% extends "base.html" %} {% block content %} 2 <h1>{{ section.title }}</h1> 3 4 + {{ section.content | safe }} {% if paginator %} {% set pages = paginator.pages 5 + %} {% else %} {% set pages = section.pages %} {% endif %} 6 7 <ul class="title-list"> 8 + {% for page in pages %} 9 + <li> 10 + <a href="{{ page.permalink | safe }}">{{ page.title }}</a> 11 + </li> 12 + {% endfor %} 13 </ul> 14 15 {% if paginator %} 16 + <div class="accent-data"> 17 + {% if paginator.previous %}<a href="{{ paginator.first }}">โฅถ</a> &nbsp 18 + <a href="{{ paginator.previous }}"><</a>{% endif %} &nbsp {{ 19 + paginator.current_index }} / {{ paginator.number_pagers }} &nbsp {% if 20 + paginator.next %}<a href="{{ paginator.next }}">></a> &nbsp 21 + <a href="{{ paginator.last }}">โฅธ</a>{% endif %} 22 + </div> 23 + {% endif %} {% endblock content %}
+1 -1
templates/shortcodes/age.md
··· 1 - {% set result = 1 %}{% for _ in range(end=length) %}{% set_global result = result * 10 %}{% endfor %}{% set time = now() | date(format="%s") | int - 1209254400 %}{{ time / 31536000 * result | round() / result }}
··· 1 + {% set time = now() | date(format="%s") | int - 1209254400 %}{{ (time / 31536000) | round(method="floor", precision=length) }}{% if comma %},{% endif %}
+70
templates/shortcodes/bluesky.html
···
··· 1 + {% set profile_part = post | split(pat="profile/") | last %} {% set parts = 2 + profile_part | split(pat="/") %} {% set handle = parts[0] %} {% set post_id = 3 + parts[2] %} {% set api_url = 4 + "https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://" ~ 5 + handle ~ "/app.bsky.feed.post/" ~ post_id %} {% set response = 6 + load_data(url=api_url, format="json") %} {% if response.thread and 7 + response.thread.post %} {% set post_data = response.thread.post %} {% set author 8 + = post_data.author.displayName %} {% set handle = post_data.author.handle %} {% 9 + set content = post_data.record.text %} {% set has_embed = post_data.embed is 10 + defined and post_data.embed %} 11 + <blockquote> 12 + {{ content }} {% if has_embed %} {% if post_data.embed["$type"] == 13 + "app.bsky.embed.video#view" %} 14 + <video controls poster="{{ post_data.embed.thumbnail }}"> 15 + <source 16 + src="{{ post_data.embed.playlist }}" 17 + type="application/x-mpegURL" 18 + /> 19 + </video> 20 + {% elif post_data.embed["$type"] == "app.bsky.embed.images#view" %} {% if 21 + post_data.embed.images | length > 3 %} 22 + <div class="image-gallery gallery-grid"> 23 + {% for image in post_data.embed.images %} 24 + <img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" /> 25 + {% endfor %} 26 + </div> 27 + {% elif post_data.embed.images | length == 2 %} 28 + <div class="image-gallery side-by-side"> 29 + {% for image in post_data.embed.images %} 30 + <img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" /> 31 + {% endfor %} 32 + </div> 33 + {% else %} 34 + <div class="image-gallery"> 35 + {% for image in post_data.embed.images %} 36 + <img src="{{ image.fullsize }}" alt="{{ image.alt }}" loading="lazy" /> 37 + {% endfor %} 38 + </div> 39 + {% endif %} {% endif %} {% endif %} 40 + </blockquote> 41 + <p> 42 + <cite> 43 + <a href="{{ post }}" target="_blank" rel="noopener" 44 + ><img 45 + src="{{ post_data.author.avatar }}" 46 + alt="{{ author }}'s avatar" 47 + class="avatar" 48 + />@{{ handle }}</a 49 + ></cite 50 + > 51 + </p> 52 + {% else %} 53 + <blockquote> 54 + <div class="bsky-post"> 55 + <div class="bsky-post-content">"Failed to render Bluesky post"</div> 56 + <div class="bsky-post-footer"> 57 + <cite 58 + ><img 59 + src="/img/bluesky-logo.png" 60 + alt="Bluesky logo" 61 + class="avatar" 62 + /> 63 + <a href="{{ post }}" target="_blank" rel="noopener" 64 + >View on Bluesky</a 65 + ></cite 66 + > 67 + </div> 68 + </div> 69 + </blockquote> 70 + {% endif %}
+42
templates/shortcodes/callout.html
···
··· 1 + {%- set type = type | default(value="info") | lower -%} 2 + {%- set title = title | default(value="") -%} 3 + 4 + {%- if type == "info" -%} 5 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' -%} 6 + {%- set color = "blue" -%} 7 + {%- set default_title = "Info" -%} 8 + {%- elif type == "warning" or type == "warn" -%} 9 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>' -%} 10 + {%- set color = "yellow" -%} 11 + {%- set default_title = "Warning" -%} 12 + {%- elif type == "danger" or type == "error" -%} 13 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' -%} 14 + {%- set color = "red" -%} 15 + {%- set default_title = "Danger" -%} 16 + {%- elif type == "tip" or type == "hint" -%} 17 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path><path d="M9 18h6"></path><path d="M10 22h4"></path></svg>' -%} 18 + {%- set color = "green" -%} 19 + {%- set default_title = "Tip" -%} 20 + {%- elif type == "note" -%} 21 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>' -%} 22 + {%- set color = "gray" -%} 23 + {%- set default_title = "Note" -%} 24 + {%- else -%} 25 + {%- set icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' -%} 26 + {%- set color = "blue" -%} 27 + {%- set default_title = "Info" -%} 28 + {%- endif -%} 29 + 30 + {%- if title == "" -%} 31 + {%- set title = default_title -%} 32 + {%- endif -%} 33 + 34 + <div class="callout callout-{{ color }}"> 35 + <div class="callout-title"> 36 + <span class="callout-icon">{{ icon | safe }}</span> 37 + <strong>{{ title }}</strong> 38 + </div> 39 + <div class="callout-content"> 40 + {{ body | markdown | safe }} 41 + </div> 42 + </div>
+1275
templates/shortcodes/frcRebuilt.html
···
··· 1 + <div id="frc-calculator" class="frc-calculator"> 2 + <div class="frc-calculator-inner"> 3 + <div class="calc-section"> 4 + <h3>Robot Parameters</h3> 5 + 6 + <div class="input-group"> 7 + <label for="ballCapacity">Ball Capacity:</label> 8 + <input type="number" id="ballCapacity" value="8" min="1" max="10"> 9 + <span class="unit">balls</span> 10 + </div> 11 + 12 + <div class="input-group"> 13 + <label for="reloadTime">Reload Time:</label> 14 + <input type="number" id="reloadTime" value="3" min="0" step="0.5"> 15 + <span class="unit">seconds</span> 16 + </div> 17 + 18 + <div class="input-group"> 19 + <label for="shooterBPS">Shooter BPS (max):</label> 20 + <input type="number" id="shooterBPS" value="4" min="0.1" step="0.1"> 21 + <span class="unit">balls/s</span> 22 + </div> 23 + 24 + <div class="input-group"> 25 + <label for="numRobots">Number of Robots:</label> 26 + <input type="number" id="numRobots" value="3" min="1" max="3"> 27 + <span class="unit">robots</span> 28 + </div> 29 + 30 + <div class="input-group"> 31 + <label for="graceTime">Hub Grace Period:</label> 32 + <input type="number" id="graceTime" value="3" min="0" max="10" step="0.5"> 33 + <span class="unit">seconds</span> 34 + </div> 35 + </div> 36 + 37 + <div class="calc-section"> 38 + <h3>Match Strategy</h3> 39 + 40 + <div class="input-group checkbox-group"> 41 + <input type="checkbox" id="includeAuto" checked> 42 + <label for="includeAuto">Include Autonomous Period</label> 43 + </div> 44 + 45 + <div class="input-group indent-group" id="autoTimeGroup"> 46 + <label for="autoShootTime">Auto Shooting Time:</label> 47 + <input type="number" id="autoShootTime" value="20" min="0" max="20" step="0.5"> 48 + <span class="unit">seconds</span> 49 + </div> 50 + 51 + <div class="input-group checkbox-group"> 52 + <input type="checkbox" id="includeEndgame" checked> 53 + <label for="includeEndgame">Include Endgame Period</label> 54 + </div> 55 + 56 + <div class="input-group indent-group" id="endgameTimeGroup"> 57 + <label for="endgameShootTime">Endgame Shooting Time:</label> 58 + <input type="number" id="endgameShootTime" value="30" min="0" max="30" step="0.5"> 59 + <span class="unit">seconds</span> 60 + </div> 61 + 62 + <div class="input-group checkbox-group"> 63 + <input type="checkbox" id="wonAuto"> 64 + <label for="wonAuto">Won Autonomous Period</label> 65 + </div> 66 + </div> 67 + 68 + <div class="calc-section"> 69 + <h3>Ranking Point Thresholds</h3> 70 + 71 + <div class="input-group"> 72 + <label for="energizedThreshold">Energized Threshold:</label> 73 + <input type="number" id="energizedThreshold" value="100" min="1" max="500"> 74 + <span class="unit">fuel</span> 75 + </div> 76 + 77 + <div class="input-group"> 78 + <label for="superchargedThreshold">Supercharged Threshold:</label> 79 + <input type="number" id="superchargedThreshold" value="360" min="1" max="1000"> 80 + <span class="unit">fuel</span> 81 + </div> 82 + </div> 83 + 84 + <div class="calc-section animation"> 85 + <h3>Match Simulation</h3> 86 + 87 + <div class="simulation-container"> 88 + <div class="simulation-header"> 89 + <div class="sim-stat"> 90 + <span class="sim-label">Time:</span> 91 + <span class="sim-value" id="simTime">0.0s</span> 92 + </div> 93 + <div class="sim-stat"> 94 + <span class="sim-label">Balls in Hopper:</span> 95 + <span class="sim-value" id="simBalls">0</span> 96 + </div> 97 + <div class="sim-stat"> 98 + <span class="sim-label">Total Scored:</span> 99 + <span class="sim-value" id="simScored">0</span> 100 + </div> 101 + <div class="sim-stat"> 102 + <span class="sim-label">Status:</span> 103 + <span class="sim-value" id="simStatus">Ready</span> 104 + </div> 105 + </div> 106 + 107 + <canvas id="matchCanvas" width="800" height="200"></canvas> 108 + 109 + <div class="timeline-container"> 110 + <div class="timeline-labels" id="timelineLabels"></div> 111 + <input type="range" id="timelineScrubber" min="0" max="168" value="0" step="0.1" class="timeline-scrubber"> 112 + </div> 113 + 114 + <div class="simulation-controls"> 115 + <button id="playPauseBtn" class="sim-button">Play</button> 116 + <button id="resetBtn" class="sim-button">Reset</button> 117 + <div class="speed-controls"> 118 + <label for="speedSelect">Speed:</label> 119 + <select id="speedSelect"> 120 + <option value="0.5">0.5x</option> 121 + <option value="1" selected>1x</option> 122 + <option value="2">2x</option> 123 + <option value="4">4x</option> 124 + <option value="8">8x</option> 125 + </select> 126 + </div> 127 + </div> 128 + </div> 129 + </div> 130 + 131 + <div class="calc-section results"> 132 + <h3>Results</h3> 133 + 134 + <div class="result-box"> 135 + <div class="result-label">Total Active Scoring Time:</div> 136 + <div class="result-value" id="totalTime">-</div> 137 + </div> 138 + 139 + <div class="result-box"> 140 + <div class="result-label">Effective Cycle Time:</div> 141 + <div class="result-value" id="cycleTime">-</div> 142 + </div> 143 + 144 + <div class="result-box highlight"> 145 + <div class="result-label">Alliance BPS for Energized RP:</div> 146 + <div class="result-value" id="energizedAllianceBPS">-</div> 147 + </div> 148 + 149 + <div class="result-box"> 150 + <div class="result-label">Per Robot BPS for Energized RP:</div> 151 + <div class="result-value" id="energizedRobotBPS">-</div> 152 + </div> 153 + 154 + <div class="result-box highlight"> 155 + <div class="result-label">Alliance BPS for Supercharged RP:</div> 156 + <div class="result-value" id="superchargedAllianceBPS">-</div> 157 + </div> 158 + 159 + <div class="result-box"> 160 + <div class="result-label">Per Robot BPS for Supercharged RP:</div> 161 + <div class="result-value" id="superchargedRobotBPS">-</div> 162 + </div> 163 + </div> 164 + </div> 165 + </div> 166 + 167 + <style> 168 + .frc-calculator { 169 + background-color: var(--accent); 170 + border-bottom: 5px solid var(--bg-light); 171 + border-radius: 7px 7px 10px 10px; 172 + padding: 0.75rem; 173 + margin: 2rem 0; 174 + } 175 + 176 + .frc-calculator-inner { 177 + background-color: var(--nightshade-violet); 178 + border-radius: 0.3rem; 179 + padding: 1rem; 180 + } 181 + 182 + .calc-section { 183 + margin-bottom: 1.5rem; 184 + } 185 + 186 + .calc-section:last-child { 187 + margin-bottom: 0; 188 + } 189 + 190 + .calc-section h3 { 191 + margin: 0 0 1rem 0; 192 + padding: 0.22em 0.4em 0.22em 0.4em; 193 + font-size: 1.25rem; 194 + background-color: var(--accent); 195 + border-bottom: 5px solid var(--bg-light); 196 + border-radius: 0.2em 0.2em 0.27em 0.27em; 197 + color: var(--accent-text); 198 + width: fit-content; 199 + } 200 + 201 + .input-group { 202 + display: flex; 203 + align-items: center; 204 + gap: 0.75rem; 205 + margin-bottom: 0.75rem; 206 + } 207 + 208 + .input-group label { 209 + flex: 1; 210 + font-weight: 500; 211 + color: var(--text); 212 + } 213 + 214 + .input-group input[type="number"] { 215 + width: 100px; 216 + padding: 0.5rem; 217 + border: 2px solid var(--ultra-violet); 218 + border-radius: var(--standard-border-radius); 219 + background-color: var(--bg); 220 + color: var(--text); 221 + font-size: 1rem; 222 + font-family: inherit; 223 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 224 + transition: border-color 120ms ease, box-shadow 120ms ease; 225 + } 226 + 227 + .input-group input[type="number"]:focus { 228 + outline: none; 229 + border-color: var(--rose-quartz); 230 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); 231 + } 232 + 233 + .input-group .unit { 234 + color: var(--text-light); 235 + font-size: 0.875rem; 236 + min-width: 60px; 237 + } 238 + 239 + .checkbox-group { 240 + margin-bottom: 0.5rem; 241 + } 242 + 243 + .checkbox-group input[type="checkbox"] { 244 + vertical-align: middle; 245 + position: relative; 246 + width: 16px; 247 + height: 16px; 248 + cursor: pointer; 249 + margin: 0; 250 + margin-right: 0.5rem; 251 + border: 2px solid var(--ultra-violet); 252 + border-radius: var(--standard-border-radius); 253 + background-color: var(--bg); 254 + transition: all 120ms ease; 255 + } 256 + 257 + .checkbox-group input[type="checkbox"]:checked { 258 + background-color: var(--rose-quartz); 259 + border-color: var(--rose-quartz); 260 + } 261 + 262 + .checkbox-group input[type="checkbox"]:hover { 263 + border-color: var(--pink-puree); 264 + } 265 + 266 + .checkbox-group label { 267 + cursor: pointer; 268 + user-select: none; 269 + display: inline-block; 270 + } 271 + 272 + .indent-group { 273 + margin-left: 2rem; 274 + margin-bottom: 1rem; 275 + } 276 + 277 + .results { 278 + background-color: var(--purple-night); 279 + padding: 1rem; 280 + border-radius: 0.3rem; 281 + border: 2px solid var(--ultra-violet); 282 + } 283 + 284 + .result-box { 285 + display: flex; 286 + justify-content: space-between; 287 + align-items: center; 288 + padding: 0.75rem; 289 + margin-bottom: 0.5rem; 290 + background-color: var(--bg); 291 + border-radius: var(--standard-border-radius); 292 + } 293 + 294 + .result-box:last-child { 295 + margin-bottom: 0; 296 + } 297 + 298 + .result-box.highlight { 299 + background-color: var(--ultra-violet); 300 + border: 1px solid var(--rose-quartz); 301 + } 302 + 303 + .result-label { 304 + font-weight: 500; 305 + color: var(--text); 306 + } 307 + 308 + .result-value { 309 + font-weight: 700; 310 + font-size: 1.125rem; 311 + color: var(--pink-puree); 312 + font-family: var(--mono-font); 313 + } 314 + 315 + @media (max-width: 640px) { 316 + .input-group { 317 + flex-direction: column; 318 + align-items: flex-start; 319 + } 320 + 321 + .input-group label { 322 + margin-bottom: 0.25rem; 323 + } 324 + 325 + .input-group input[type="number"] { 326 + width: 100%; 327 + } 328 + 329 + .result-box { 330 + flex-direction: column; 331 + align-items: flex-start; 332 + gap: 0.5rem; 333 + } 334 + 335 + #matchCanvas { 336 + width: 100%; 337 + height: auto; 338 + } 339 + } 340 + 341 + .simulation-container { 342 + margin-top: 1rem; 343 + } 344 + 345 + .simulation-header { 346 + display: grid; 347 + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 348 + gap: 0.75rem; 349 + margin-bottom: 1rem; 350 + padding: 0.75rem; 351 + background-color: var(--bg); 352 + border-radius: var(--standard-border-radius); 353 + border: 2px solid var(--ultra-violet); 354 + } 355 + 356 + .sim-stat { 357 + display: flex; 358 + flex-direction: column; 359 + gap: 0.25rem; 360 + } 361 + 362 + .sim-label { 363 + font-size: 0.75rem; 364 + color: var(--text-light); 365 + font-weight: 600; 366 + } 367 + 368 + .sim-value { 369 + font-size: 1rem; 370 + color: var(--pink-puree); 371 + font-family: var(--mono-font); 372 + font-weight: 700; 373 + } 374 + 375 + #matchCanvas { 376 + width: 100%; 377 + height: 200px; 378 + background-color: var(--nightshade-violet); 379 + border-radius: var(--standard-border-radius); 380 + border: 2px solid var(--ultra-violet); 381 + display: block; 382 + } 383 + 384 + .timeline-container { 385 + margin-top: 1rem; 386 + position: relative; 387 + } 388 + 389 + .timeline-labels { 390 + display: flex; 391 + justify-content: space-between; 392 + margin-bottom: 0.5rem; 393 + font-size: 0.75rem; 394 + color: var(--text-light); 395 + padding: 0 0.5rem; 396 + } 397 + 398 + .timeline-scrubber { 399 + width: 100%; 400 + height: 8px; 401 + border-radius: 4px; 402 + background: var(--bg); 403 + border: 2px solid var(--ultra-violet); 404 + outline: none; 405 + cursor: pointer; 406 + -webkit-appearance: none; 407 + appearance: none; 408 + } 409 + 410 + .timeline-scrubber::-webkit-slider-thumb { 411 + -webkit-appearance: none; 412 + appearance: none; 413 + width: 16px; 414 + height: 16px; 415 + border-radius: 50%; 416 + background: var(--rose-quartz); 417 + cursor: pointer; 418 + border: 2px solid var(--pink-puree); 419 + } 420 + 421 + .timeline-scrubber::-moz-range-thumb { 422 + width: 16px; 423 + height: 16px; 424 + border-radius: 50%; 425 + background: var(--rose-quartz); 426 + cursor: pointer; 427 + border: 2px solid var(--pink-puree); 428 + } 429 + 430 + .simulation-controls { 431 + display: flex; 432 + gap: 0.75rem; 433 + align-items: center; 434 + margin-top: 1rem; 435 + flex-wrap: wrap; 436 + } 437 + 438 + .sim-button { 439 + padding: 0.5rem 1rem; 440 + background-color: var(--accent); 441 + color: var(--accent-text); 442 + border: 2px solid var(--ultra-violet); 443 + border-radius: var(--standard-border-radius); 444 + font-weight: 600; 445 + cursor: pointer; 446 + transition: all 120ms ease; 447 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 448 + } 449 + 450 + .sim-button:hover { 451 + background-color: var(--rose-quartz); 452 + border-color: var(--pink-puree); 453 + } 454 + 455 + .sim-button:active { 456 + transform: translateY(1px); 457 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 458 + } 459 + 460 + .speed-controls { 461 + display: flex; 462 + align-items: center; 463 + gap: 0.5rem; 464 + margin-left: auto; 465 + } 466 + 467 + .speed-controls label { 468 + font-size: 0.875rem; 469 + color: var(--text); 470 + font-weight: 600; 471 + } 472 + 473 + .speed-controls select { 474 + padding: 0.5rem; 475 + background-color: var(--bg); 476 + color: var(--text); 477 + border: 2px solid var(--ultra-violet); 478 + border-radius: var(--standard-border-radius); 479 + font-family: inherit; 480 + cursor: pointer; 481 + } 482 + </style> 483 + 484 + <script> 485 + (function() { 486 + // Wait for DOM to be ready 487 + if (document.readyState === 'loading') { 488 + document.addEventListener('DOMContentLoaded', initCalculator); 489 + } else { 490 + initCalculator(); 491 + } 492 + 493 + function initCalculator() { 494 + const calculator = document.getElementById("frc-calculator"); 495 + if (!calculator) return; 496 + 497 + // Get all input elements 498 + const inputs = { 499 + ballCapacity: document.getElementById("ballCapacity"), 500 + reloadTime: document.getElementById("reloadTime"), 501 + shooterBPS: document.getElementById("shooterBPS"), 502 + numRobots: document.getElementById("numRobots"), 503 + graceTime: document.getElementById("graceTime"), 504 + includeAuto: document.getElementById("includeAuto"), 505 + autoShootTime: document.getElementById("autoShootTime"), 506 + includeEndgame: document.getElementById("includeEndgame"), 507 + endgameShootTime: document.getElementById("endgameShootTime"), 508 + wonAuto: document.getElementById("wonAuto"), 509 + energizedThreshold: document.getElementById("energizedThreshold"), 510 + superchargedThreshold: document.getElementById("superchargedThreshold"), 511 + }; 512 + 513 + // Get all result elements 514 + const results = { 515 + totalTime: document.getElementById("totalTime"), 516 + cycleTime: document.getElementById("cycleTime"), 517 + energizedAllianceBPS: document.getElementById("energizedAllianceBPS"), 518 + energizedRobotBPS: document.getElementById("energizedRobotBPS"), 519 + superchargedAllianceBPS: document.getElementById("superchargedAllianceBPS"), 520 + superchargedRobotBPS: document.getElementById("superchargedRobotBPS"), 521 + }; 522 + 523 + // Toggle visibility of conditional inputs 524 + const autoTimeGroup = document.getElementById("autoTimeGroup"); 525 + const endgameTimeGroup = document.getElementById("endgameTimeGroup"); 526 + 527 + inputs.includeAuto.addEventListener("change", () => { 528 + autoTimeGroup.style.display = inputs.includeAuto.checked ? "flex" : "none"; 529 + calculate(); 530 + }); 531 + 532 + inputs.includeEndgame.addEventListener("change", () => { 533 + endgameTimeGroup.style.display = inputs.includeEndgame.checked ? "flex" : "none"; 534 + calculate(); 535 + }); 536 + 537 + // Add event listeners to all inputs 538 + Object.values(inputs).forEach((input) => { 539 + input.addEventListener("input", calculate); 540 + input.addEventListener("change", calculate); 541 + }); 542 + 543 + function calculate() { 544 + // Get input values 545 + const ballCapacity = parseFloat(inputs.ballCapacity.value) || 0; 546 + const reloadTime = parseFloat(inputs.reloadTime.value) || 0; 547 + const shooterBPS = parseFloat(inputs.shooterBPS.value) || 4; 548 + const numRobots = parseFloat(inputs.numRobots.value) || 1; 549 + const includeAuto = inputs.includeAuto.checked; 550 + const autoShootTime = parseFloat(inputs.autoShootTime.value) || 0; 551 + const includeEndgame = inputs.includeEndgame.checked; 552 + const endgameShootTime = parseFloat(inputs.endgameShootTime.value) || 0; 553 + const wonAuto = inputs.wonAuto.checked; 554 + const energizedThreshold = parseFloat(inputs.energizedThreshold.value) || 0; 555 + const superchargedThreshold = parseFloat(inputs.superchargedThreshold.value) || 0; 556 + 557 + // Calculate total active scoring time 558 + let totalTime = 0; 559 + 560 + // Auto period (if included) 561 + if (includeAuto) { 562 + totalTime += autoShootTime; 563 + } 564 + 565 + // Transition period (always active, now 10s) 566 + totalTime += 10; 567 + 568 + // Shifts - 2 active shifts of 25s each 569 + // If won auto: Shifts 2 & 4 are active 570 + // If lost auto: Shifts 1 & 3 are active 571 + totalTime += 50; // 2 shifts ร— 25s 572 + 573 + // Endgame period (if included, now 30s) 574 + if (includeEndgame) { 575 + totalTime += endgameShootTime; 576 + } 577 + 578 + // Calculate free reloads during inactive hub time 579 + // There are 58 seconds of inactive time (8s break + 2 shifts ร— 25s) 580 + // This is the ONLY time we can reload without losing active scoring time 581 + const inactiveTime = 58; 582 + const freeReloads = reloadTime > 0 ? Math.floor(inactiveTime / reloadTime) : 0; 583 + 584 + // BPS calculation accounting for reload time during auto/endgame 585 + function calculateBPSForThreshold(threshold) { 586 + if (totalTime <= 0 || threshold <= 0 || ballCapacity <= 0) return 0; 587 + 588 + // Calculate cycles needed to reach threshold 589 + const cyclesNeeded = Math.ceil(threshold / ballCapacity); 590 + const reloadsNeeded = Math.max(0, cyclesNeeded - 1); 591 + 592 + // Reloads during inactive time are free 593 + // Remaining reloads consume active time 594 + const paidReloads = Math.max(0, reloadsNeeded - freeReloads); 595 + 596 + // Available shooting time = total active time - paid reload time 597 + const shootingTime = totalTime - (paidReloads * reloadTime); 598 + 599 + // BPS = threshold / available shooting time 600 + return shootingTime > 0 ? threshold / shootingTime : 0; 601 + } 602 + 603 + // Calculate max balls achievable by simulating full match 604 + function calculateMaxBalls() { 605 + if (shooterBPS <= 0 || ballCapacity <= 0) return 0; 606 + 607 + // Run simulation to completion (t=168s) with current settings 608 + let totalScored = 0; 609 + let cyclePosition = 0; // 0 = ready to shoot, 1 = reloading 610 + let reloadProgress = 0; 611 + let ballsInHopper = Math.min(ballCapacity, 8); // Preload 612 + 613 + const includeAuto = inputs.includeAuto.checked; 614 + const autoShootTime = parseFloat(inputs.autoShootTime.value) || 0; 615 + const includeEndgame = inputs.includeEndgame.checked; 616 + const endgameShootTime = parseFloat(inputs.endgameShootTime.value) || 0; 617 + 618 + for (let periodIndex = 0; periodIndex < periods.length; periodIndex++) { 619 + const period = periods[periodIndex]; 620 + 621 + // Skip BREAK period 622 + if (period.name === "BREAK") continue; 623 + 624 + // Check if hub is active for this period 625 + let periodActive = period.active; 626 + if (period.name === "S1" || period.name === "S3") { 627 + periodActive = !wonAuto; 628 + } else if (period.name === "S2" || period.name === "S4") { 629 + periodActive = wonAuto; 630 + } 631 + 632 + // Determine effective time and if shooting is allowed 633 + const timeInPeriod = period.end - period.start; 634 + let effectiveTimeInPeriod = timeInPeriod; 635 + let canShootInPeriod = true; 636 + 637 + if (period.name === "AUTO") { 638 + if (!includeAuto) { 639 + canShootInPeriod = false; 640 + } else { 641 + effectiveTimeInPeriod = Math.min(timeInPeriod, autoShootTime); 642 + } 643 + } else if (period.name === "END") { 644 + if (!includeEndgame) { 645 + canShootInPeriod = false; 646 + } else { 647 + effectiveTimeInPeriod = Math.min(timeInPeriod, endgameShootTime); 648 + } 649 + } 650 + 651 + let timeRemaining = effectiveTimeInPeriod; 652 + 653 + // Check grace period eligibility 654 + let allowGraceShoot = false; 655 + if (!periodActive && periodIndex > 0 && period.name !== "BREAK") { 656 + const prevPeriod = periods[periodIndex - 1]; 657 + let prevActive = prevPeriod.active; 658 + if (prevPeriod.name === "S1" || prevPeriod.name === "S3") { 659 + prevActive = !wonAuto; 660 + } else if (prevPeriod.name === "S2" || prevPeriod.name === "S4") { 661 + prevActive = wonAuto; 662 + } 663 + if (prevActive) { 664 + allowGraceShoot = true; 665 + } 666 + } 667 + 668 + // Simulate this period 669 + let timeIntoPeriod = 0; 670 + while (timeRemaining > 0) { 671 + const canShootWithGrace = periodActive || (allowGraceShoot && timeIntoPeriod < graceTime); 672 + 673 + if (cyclePosition === 0) { 674 + // Ready to shoot 675 + if (canShootWithGrace && canShootInPeriod) { 676 + const ballsToShoot = ballsInHopper; 677 + const timeToShootAll = ballsToShoot / shooterBPS; 678 + const timeToShoot = Math.min(timeToShootAll, timeRemaining); 679 + 680 + const ballsActuallyShot = Math.floor(timeToShoot * shooterBPS); 681 + totalScored += ballsActuallyShot * numRobots; 682 + ballsInHopper -= ballsActuallyShot; 683 + timeRemaining -= timeToShoot; 684 + timeIntoPeriod += timeToShoot; 685 + 686 + if (ballsInHopper <= 0) { 687 + ballsInHopper = 0; 688 + cyclePosition = 1; 689 + reloadProgress = 0; 690 + } else { 691 + break; // Still have balls but out of time 692 + } 693 + } else { 694 + break; // Can't shoot, wait 695 + } 696 + } else if (cyclePosition === 1) { 697 + // Reloading 698 + const timeToReload = Math.min(reloadTime - reloadProgress, timeRemaining); 699 + timeRemaining -= timeToReload; 700 + timeIntoPeriod += timeToReload; 701 + reloadProgress += timeToReload; 702 + 703 + if (reloadProgress >= reloadTime) { 704 + ballsInHopper = ballCapacity; 705 + cyclePosition = 0; 706 + reloadProgress = 0; 707 + } else { 708 + break; // Still reloading 709 + } 710 + } 711 + } 712 + } 713 + 714 + return totalScored; 715 + } 716 + 717 + const energizedAllianceBPS = calculateBPSForThreshold(energizedThreshold); 718 + const superchargedAllianceBPS = calculateBPSForThreshold(superchargedThreshold); 719 + const maxBalls = calculateMaxBalls(); 720 + 721 + // Calculate per robot BPS 722 + const energizedRobotBPS = numRobots > 0 ? energizedAllianceBPS / numRobots : 0; 723 + const superchargedRobotBPS = numRobots > 0 ? superchargedAllianceBPS / numRobots : 0; 724 + 725 + // Cycle time is the time to shoot all balls in the hopper plus reload time 726 + // Based on physical shooter speed (not target BPS) 727 + const shootingTimePerCycle = ballCapacity > 0 && shooterBPS > 0 ? ballCapacity / shooterBPS : 0; 728 + const avgCycleTime = shootingTimePerCycle + reloadTime; 729 + 730 + // Update results 731 + results.totalTime.textContent = `${totalTime.toFixed(1)}s`; 732 + results.cycleTime.textContent = avgCycleTime > 0 ? `${avgCycleTime.toFixed(2)}s` : "N/A"; 733 + 734 + // Show BPS or "N/A (max X balls)" if unreachable 735 + if (energizedAllianceBPS > 0 && maxBalls >= energizedThreshold) { 736 + results.energizedAllianceBPS.textContent = `${energizedAllianceBPS.toFixed(2)} balls/s`; 737 + results.energizedRobotBPS.textContent = `${energizedRobotBPS.toFixed(2)} balls/s`; 738 + } else { 739 + results.energizedAllianceBPS.textContent = `N/A (max ${maxBalls} balls)`; 740 + results.energizedRobotBPS.textContent = `N/A (max ${maxBalls} balls)`; 741 + } 742 + 743 + if (superchargedAllianceBPS > 0 && maxBalls >= superchargedThreshold) { 744 + results.superchargedAllianceBPS.textContent = `${superchargedAllianceBPS.toFixed(2)} balls/s`; 745 + results.superchargedRobotBPS.textContent = `${superchargedRobotBPS.toFixed(2)} balls/s`; 746 + } else { 747 + results.superchargedAllianceBPS.textContent = `N/A (max ${maxBalls} balls)`; 748 + results.superchargedRobotBPS.textContent = `N/A (max ${maxBalls} balls)`; 749 + } 750 + } 751 + 752 + // Match Simulation 753 + const canvas = document.getElementById("matchCanvas"); 754 + const ctx = canvas ? canvas.getContext("2d") : null; 755 + const timelineScrubber = document.getElementById("timelineScrubber"); 756 + const playPauseBtn = document.getElementById("playPauseBtn"); 757 + const resetBtn = document.getElementById("resetBtn"); 758 + const speedSelect = document.getElementById("speedSelect"); 759 + const simTime = document.getElementById("simTime"); 760 + const simBalls = document.getElementById("simBalls"); 761 + const simScored = document.getElementById("simScored"); 762 + const simStatus = document.getElementById("simStatus"); 763 + const timelineLabels = document.getElementById("timelineLabels"); 764 + 765 + let isPlaying = false; 766 + let currentTime = 0; 767 + let animationFrame = null; 768 + let lastTimestamp = 0; 769 + 770 + // Match timeline periods 771 + const periods = [ 772 + { name: "AUTO", start: 0, end: 20, active: true }, 773 + { name: "BREAK", start: 20, end: 28, active: false }, 774 + { name: "TRANS", start: 28, end: 38, active: true }, 775 + { name: "S1", start: 38, end: 63, active: false }, 776 + { name: "S2", start: 63, end: 88, active: true }, 777 + { name: "S3", start: 88, end: 113, active: false }, 778 + { name: "S4", start: 113, end: 138, active: true }, 779 + { name: "END", start: 138, end: 168, active: true } 780 + ]; 781 + 782 + function setupTimeline() { 783 + if (!timelineLabels) return; 784 + 785 + timelineLabels.innerHTML = periods.map(p => `<span>${p.name}</span>`).join(''); 786 + } 787 + 788 + function drawCanvas() { 789 + if (!ctx || !canvas) return; 790 + 791 + const width = canvas.width; 792 + const height = canvas.height; 793 + const ballCapacity = parseFloat(inputs.ballCapacity.value) || 3; 794 + const reloadTime = parseFloat(inputs.reloadTime.value) || 5; 795 + const shooterBPS = parseFloat(inputs.shooterBPS.value) || 4; 796 + 797 + // Clear canvas 798 + ctx.fillStyle = '#1e1e2e'; // nightshade-violet 799 + ctx.fillRect(0, 0, width, height); 800 + 801 + // Determine current period 802 + const wonAuto = inputs.wonAuto.checked; 803 + let currentPeriod = periods.find(p => currentTime >= p.start && currentTime < p.end); 804 + if (!currentPeriod) currentPeriod = periods[periods.length - 1]; 805 + 806 + // Draw timeline periods 807 + periods.forEach(period => { 808 + const x = (period.start / 168) * width; 809 + const w = ((period.end - period.start) / 168) * width; 810 + 811 + // Determine if hub is active based on wonAuto 812 + let isActive = period.active; 813 + 814 + if (period.name === "S1" || period.name === "S3") { 815 + isActive = !wonAuto; 816 + } else if (period.name === "S2" || period.name === "S4") { 817 + isActive = wonAuto; 818 + } 819 + 820 + // Check if this is the current period 821 + const isCurrent = period === currentPeriod; 822 + 823 + // Background color - brighter if current period 824 + if (isCurrent) { 825 + ctx.fillStyle = isActive ? 'rgba(166, 218, 149, 0.4)' : 'rgba(237, 135, 150, 0.4)'; 826 + } else { 827 + ctx.fillStyle = isActive ? 'rgba(166, 218, 149, 0.15)' : 'rgba(237, 135, 150, 0.15)'; 828 + } 829 + ctx.fillRect(x, 0, w, height); 830 + 831 + // Draw period border - thicker if current 832 + ctx.strokeStyle = isCurrent ? '#8aadf4' : 'rgba(138, 173, 244, 0.3)'; 833 + ctx.lineWidth = isCurrent ? 2 : 1; 834 + ctx.strokeRect(x, 0, w, height); 835 + 836 + // Draw period label - bold if current 837 + ctx.fillStyle = '#cad3f5'; 838 + ctx.font = isCurrent ? 'bold 14px monospace' : '12px monospace'; 839 + ctx.textAlign = 'center'; 840 + ctx.fillText(period.name, x + w / 2, isCurrent ? 22 : 20); 841 + }); 842 + 843 + // Simulate robot state at current time 844 + let ballsInHopper = 0; 845 + let totalScored = 0; 846 + let status = "Idle"; 847 + let timeInCycle = 0; 848 + const numRobots = Math.min(3, parseFloat(inputs.numRobots.value) || 1); 849 + const graceTime = parseFloat(inputs.graceTime.value) || 0; 850 + 851 + // Helper function to check if we're in grace period after hub goes inactive 852 + function isInGracePeriod(time) { 853 + // Find which period we're in 854 + const currentPeriodIndex = periods.findIndex(p => time >= p.start && time < p.end); 855 + if (currentPeriodIndex === -1) return false; 856 + 857 + const currentPer = periods[currentPeriodIndex]; 858 + 859 + // Check if previous period was active and current is inactive 860 + if (currentPeriodIndex > 0) { 861 + const prevPeriod = periods[currentPeriodIndex - 1]; 862 + 863 + let prevActive = prevPeriod.active; 864 + let currActive = currentPer.active; 865 + 866 + if (prevPeriod.name === "S1" || prevPeriod.name === "S3") { 867 + prevActive = !wonAuto; 868 + } else if (prevPeriod.name === "S2" || prevPeriod.name === "S4") { 869 + prevActive = wonAuto; 870 + } 871 + 872 + if (currentPer.name === "S1" || currentPer.name === "S3") { 873 + currActive = !wonAuto; 874 + } else if (currentPer.name === "S2" || currentPer.name === "S4") { 875 + currActive = wonAuto; 876 + } 877 + 878 + // If we went from active to inactive 879 + if (prevActive && !currActive) { 880 + const timeSinceTransition = time - currentPer.start; 881 + return timeSinceTransition <= graceTime; 882 + } 883 + } 884 + return false; 885 + } 886 + 887 + // Check if hub is active (including grace period) 888 + let isHubActive = currentPeriod.active; 889 + if (currentPeriod.name === "S1" || currentPeriod.name === "S3") { 890 + isHubActive = !wonAuto; 891 + } else if (currentPeriod.name === "S2" || currentPeriod.name === "S4") { 892 + isHubActive = wonAuto; 893 + } 894 + 895 + // Check grace period 896 + if (!isHubActive && isInGracePeriod(currentTime)) { 897 + isHubActive = true; 898 + } 899 + 900 + if (shooterBPS > 0) { 901 + const shootingTimePerCycle = ballCapacity / shooterBPS; 902 + const includeAuto = inputs.includeAuto.checked; 903 + const autoShootTime = parseFloat(inputs.autoShootTime.value) || 0; 904 + const includeEndgame = inputs.includeEndgame.checked; 905 + const endgameShootTime = parseFloat(inputs.endgameShootTime.value) || 0; 906 + 907 + // Simulate through all periods up to current time 908 + let timeAccumulator = 0; 909 + let cyclePosition = 0; // 0 = ready to shoot, 1 = reloading 910 + let reloadProgress = 0; // Track partial reload progress (0 to reloadTime) 911 + 912 + // Start with preloaded balls (max 8) 913 + ballsInHopper = Math.min(ballCapacity, 8); 914 + 915 + for (let periodIndex = 0; periodIndex < periods.length; periodIndex++) { 916 + const period = periods[periodIndex]; 917 + if (currentTime < period.start) break; 918 + 919 + const periodEnd = Math.min(period.end, currentTime); 920 + const timeInPeriod = periodEnd - period.start; 921 + 922 + // Check if hub is active for this period 923 + let periodActive = period.active; 924 + if (period.name === "S1" || period.name === "S3") { 925 + periodActive = !wonAuto; 926 + } else if (period.name === "S2" || period.name === "S4") { 927 + periodActive = wonAuto; 928 + } 929 + 930 + // Limit time in period based on user settings for auto/endgame 931 + let effectiveTimeInPeriod = timeInPeriod; 932 + let canShootInPeriod = true; 933 + 934 + if (period.name === "AUTO") { 935 + if (!includeAuto) { 936 + canShootInPeriod = false; 937 + } else { 938 + // Limit to autoShootTime 939 + effectiveTimeInPeriod = Math.min(timeInPeriod, autoShootTime); 940 + } 941 + } else if (period.name === "END") { 942 + if (!includeEndgame) { 943 + canShootInPeriod = false; 944 + } else { 945 + // Limit to endgameShootTime 946 + effectiveTimeInPeriod = Math.min(timeInPeriod, endgameShootTime); 947 + } 948 + } else if (period.name === "BREAK") { 949 + // No activity during break - skip this period entirely 950 + if (currentTime >= period.start && currentTime <= periodEnd) { 951 + status = "Break"; 952 + } 953 + continue; // Skip to next period 954 + } 955 + 956 + let timeRemaining = effectiveTimeInPeriod; 957 + 958 + // Check if we should allow shooting in grace period 959 + let allowGraceShoot = false; 960 + if (!periodActive && periodIndex > 0 && period.name !== "BREAK") { 961 + const prevPeriod = periods[periodIndex - 1]; 962 + let prevActive = prevPeriod.active; 963 + if (prevPeriod.name === "S1" || prevPeriod.name === "S3") { 964 + prevActive = !wonAuto; 965 + } else if (prevPeriod.name === "S2" || prevPeriod.name === "S4") { 966 + prevActive = wonAuto; 967 + } 968 + 969 + // Previous period was active, current is inactive - allow grace period 970 + if (prevActive) { 971 + allowGraceShoot = true; 972 + } 973 + } 974 + 975 + while (timeRemaining > 0) { 976 + // Check if we're still in grace period 977 + const timeIntoPeriod = effectiveTimeInPeriod - timeRemaining; 978 + const canShootWithGrace = periodActive || (allowGraceShoot && timeIntoPeriod < graceTime); 979 + 980 + if (cyclePosition === 0) { 981 + // Ready to shoot (have balls loaded) 982 + if (canShootWithGrace && canShootInPeriod) { 983 + // Calculate how many balls we actually have to shoot 984 + const ballsToShoot = ballsInHopper; 985 + const timeToShootAll = ballsToShoot / shooterBPS; 986 + const timeToShoot = Math.min(timeToShootAll, timeRemaining); 987 + timeRemaining -= timeToShoot; 988 + 989 + const ballsActuallyShot = Math.floor(timeToShoot * shooterBPS); 990 + totalScored += ballsActuallyShot * numRobots; 991 + ballsInHopper -= ballsActuallyShot; 992 + 993 + if (ballsInHopper <= 0) { 994 + // Finished shooting all balls - start reload 995 + ballsInHopper = 0; 996 + cyclePosition = 1; 997 + reloadProgress = 0; 998 + 999 + if (currentTime >= period.start && currentTime <= periodEnd && timeRemaining === 0) { 1000 + status = "Reloading"; 1001 + } 1002 + } else { 1003 + // Still shooting 1004 + if (currentTime >= period.start && currentTime <= periodEnd && timeRemaining === 0) { 1005 + status = "Shooting"; 1006 + } 1007 + break; 1008 + } 1009 + } else { 1010 + // Hub inactive, can't shoot - maintain current balls 1011 + if (currentTime >= period.start && currentTime <= periodEnd) { 1012 + status = allowGraceShoot && timeIntoPeriod < graceTime ? "Grace Period" : "Idle"; 1013 + } 1014 + break; 1015 + } 1016 + } else if (cyclePosition === 1) { 1017 + // Reloading 1018 + const timeToReload = Math.min(reloadTime - reloadProgress, timeRemaining); 1019 + timeRemaining -= timeToReload; 1020 + reloadProgress += timeToReload; 1021 + 1022 + if (reloadProgress >= reloadTime) { 1023 + // Finished reloading 1024 + ballsInHopper = ballCapacity; 1025 + cyclePosition = 0; 1026 + reloadProgress = 0; 1027 + 1028 + if (currentTime >= period.start && currentTime <= periodEnd && timeRemaining === 0) { 1029 + status = canShootWithGrace ? "Ready" : "Idle"; 1030 + } 1031 + } else { 1032 + // Still reloading - show partial progress 1033 + if (currentTime >= period.start && currentTime <= periodEnd && timeRemaining === 0) { 1034 + const reloadPercent = reloadProgress / reloadTime; 1035 + ballsInHopper = Math.floor(ballCapacity * reloadPercent); 1036 + status = "Reloading"; 1037 + } 1038 + break; 1039 + } 1040 + } 1041 + } 1042 + 1043 + // Check if we're past the shooting time in auto/endgame 1044 + if (currentTime >= period.start && currentTime <= periodEnd) { 1045 + if (period.name === "AUTO" && includeAuto) { 1046 + const timeIntoPeriod = currentTime - period.start; 1047 + if (timeIntoPeriod >= autoShootTime) { 1048 + status = "Idle"; 1049 + // Keep current balls loaded 1050 + } 1051 + } else if (period.name === "END" && includeEndgame) { 1052 + const timeIntoPeriod = currentTime - period.start; 1053 + if (timeIntoPeriod >= endgameShootTime) { 1054 + status = "Idle"; 1055 + // Keep current balls loaded 1056 + } 1057 + } 1058 + } 1059 + } 1060 + } 1061 + 1062 + // Draw progress bar 1063 + const progressX = (currentTime / 168) * width; 1064 + ctx.strokeStyle = '#f5bde6'; 1065 + ctx.lineWidth = 3; 1066 + ctx.beginPath(); 1067 + ctx.moveTo(progressX, 0); 1068 + ctx.lineTo(progressX, height); 1069 + ctx.stroke(); 1070 + 1071 + // Draw robot visualizations (one per robot, max 3) 1072 + const robotWidth = 50; 1073 + const robotHeight = 40; 1074 + const robotSpacing = 80; 1075 + const startX = 40; 1076 + const robotY = height - 60; 1077 + 1078 + for (let robotIndex = 0; robotIndex < numRobots; robotIndex++) { 1079 + const robotX = startX + (robotIndex * robotSpacing); 1080 + 1081 + // Robot body - color based on status 1082 + let robotColor; 1083 + if (status === "Shooting") { 1084 + robotColor = '#a6da95'; // Green - shooting 1085 + } else if (status === "Reloading") { 1086 + robotColor = '#eed49f'; // Yellow - reloading 1087 + } else { 1088 + robotColor = '#ed8796'; // Red - idle/waiting 1089 + } 1090 + 1091 + ctx.fillStyle = robotColor; 1092 + ctx.fillRect(robotX, robotY, robotWidth, robotHeight); 1093 + ctx.strokeStyle = '#8aadf4'; 1094 + ctx.lineWidth = 2; 1095 + ctx.strokeRect(robotX, robotY, robotWidth, robotHeight); 1096 + 1097 + // Draw balls in hopper 1098 + if (status === "Reloading") { 1099 + // During reload, show balls filling up from bottom to top 1100 + // ballsInHopper already contains the partial reload count 1101 + if (ballCapacity <= 16) { 1102 + const ballRadius = 4; 1103 + const ballsPerRow = 4; 1104 + for (let i = 0; i < ballsInHopper; i++) { 1105 + const row = Math.floor(i / ballsPerRow); 1106 + const col = i % ballsPerRow; 1107 + const ballX = robotX + 6 + col * 10; 1108 + const ballY = robotY + robotHeight - 6 - row * 10; 1109 + 1110 + ctx.fillStyle = '#f5a97f'; 1111 + ctx.beginPath(); 1112 + ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2); 1113 + ctx.fill(); 1114 + } 1115 + } else { 1116 + // For large capacities, show count and progress 1117 + const reloadProgress = ballsInHopper / ballCapacity; 1118 + ctx.fillStyle = '#f5a97f'; 1119 + ctx.font = 'bold 20px monospace'; 1120 + ctx.textAlign = 'center'; 1121 + ctx.fillText(ballsInHopper, robotX + 25, robotY + 25); 1122 + 1123 + // Progress bar 1124 + ctx.fillStyle = 'rgba(245, 169, 127, 0.3)'; 1125 + ctx.fillRect(robotX + 5, robotY + robotHeight - 8, robotWidth - 10, 4); 1126 + ctx.fillStyle = '#f5a97f'; 1127 + ctx.fillRect(robotX + 5, robotY + robotHeight - 8, (robotWidth - 10) * reloadProgress, 4); 1128 + } 1129 + } else if (ballCapacity <= 16) { 1130 + // Draw individual balls for small capacities (up to 16) 1131 + const ballRadius = 4; 1132 + const ballsPerRow = 4; 1133 + for (let i = 0; i < ballsInHopper; i++) { 1134 + const row = Math.floor(i / ballsPerRow); 1135 + const col = i % ballsPerRow; 1136 + const ballX = robotX + 6 + col * 10; 1137 + const ballY = robotY + 6 + row * 10; 1138 + 1139 + ctx.fillStyle = '#f5a97f'; 1140 + ctx.beginPath(); 1141 + ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2); 1142 + ctx.fill(); 1143 + } 1144 + } else { 1145 + // For large capacities, just show the count 1146 + ctx.fillStyle = '#f5a97f'; 1147 + ctx.font = 'bold 24px monospace'; 1148 + ctx.textAlign = 'center'; 1149 + ctx.fillText(ballsInHopper, robotX + 25, robotY + 28); 1150 + } 1151 + 1152 + // Robot number label 1153 + ctx.fillStyle = '#8aadf4'; 1154 + ctx.font = 'bold 10px monospace'; 1155 + ctx.textAlign = 'center'; 1156 + ctx.fillText(`R${robotIndex + 1}`, robotX + 25, robotY - 4); 1157 + } 1158 + 1159 + // Status text (shared for all robots) 1160 + const statusX = startX + (numRobots * robotSpacing); 1161 + ctx.fillStyle = '#cad3f5'; 1162 + ctx.font = 'bold 14px monospace'; 1163 + ctx.textAlign = 'left'; 1164 + ctx.fillText(status, statusX, robotY + 20); 1165 + ctx.fillText(`Balls: ${ballsInHopper}/${ballCapacity}`, statusX, robotY + 40); 1166 + 1167 + // Update stats 1168 + // Timer display logic: countdown in AUTO, pause at 0 during BREAK, count up in teleop 1169 + // Match time is 160s (game time) but simulation runs 168s (includes 8s break) 1170 + let displayTime; 1171 + let matchTime; // Actual match time shown to drivers 1172 + 1173 + if (currentTime <= 20) { 1174 + // AUTO period: countdown from 15 to 0 1175 + matchTime = 20 - currentTime; 1176 + displayTime = matchTime.toFixed(1); 1177 + } else if (currentTime <= 28) { 1178 + // BREAK period: stays at 0 (doesn't count toward match time) 1179 + matchTime = 0; 1180 + displayTime = "0.0"; 1181 + } else { 1182 + // Teleop: count up from 0 to 140 (subtract the 8s break) 1183 + matchTime = currentTime - 8; // Remove the break time 1184 + displayTime = (matchTime - 20).toFixed(1); // Show time since auto ended 1185 + } 1186 + 1187 + if (simTime) simTime.textContent = `${displayTime}s`; 1188 + if (simBalls) simBalls.textContent = ballsInHopper; 1189 + if (simScored) simScored.textContent = totalScored; 1190 + if (simStatus) simStatus.textContent = status; 1191 + } 1192 + 1193 + function animate(timestamp) { 1194 + if (!isPlaying) return; 1195 + 1196 + if (lastTimestamp === 0) lastTimestamp = timestamp; 1197 + const deltaTime = (timestamp - lastTimestamp) / 1000; // Convert to seconds 1198 + lastTimestamp = timestamp; 1199 + 1200 + const speed = parseFloat(speedSelect.value) || 1; 1201 + currentTime += deltaTime * speed; 1202 + 1203 + if (currentTime >= 168) { 1204 + currentTime = 168; 1205 + pause(); 1206 + } 1207 + 1208 + if (timelineScrubber) timelineScrubber.value = currentTime; 1209 + drawCanvas(); 1210 + 1211 + if (isPlaying) { 1212 + animationFrame = requestAnimationFrame(animate); 1213 + } 1214 + } 1215 + 1216 + function play() { 1217 + if (currentTime >= 160) currentTime = 0; 1218 + isPlaying = true; 1219 + lastTimestamp = 0; 1220 + if (playPauseBtn) playPauseBtn.textContent = "Pause"; 1221 + animationFrame = requestAnimationFrame(animate); 1222 + } 1223 + 1224 + function pause() { 1225 + isPlaying = false; 1226 + if (playPauseBtn) playPauseBtn.textContent = "Play"; 1227 + if (animationFrame) cancelAnimationFrame(animationFrame); 1228 + } 1229 + 1230 + function reset() { 1231 + pause(); 1232 + currentTime = 0; 1233 + if (timelineScrubber) timelineScrubber.value = 0; 1234 + drawCanvas(); 1235 + } 1236 + 1237 + // Event listeners 1238 + if (playPauseBtn) { 1239 + playPauseBtn.addEventListener("click", () => { 1240 + if (isPlaying) pause(); 1241 + else play(); 1242 + }); 1243 + } 1244 + 1245 + if (resetBtn) { 1246 + resetBtn.addEventListener("click", reset); 1247 + } 1248 + 1249 + if (timelineScrubber) { 1250 + timelineScrubber.addEventListener("input", (e) => { 1251 + currentTime = parseFloat(e.target.value); 1252 + drawCanvas(); 1253 + }); 1254 + 1255 + timelineScrubber.addEventListener("mousedown", () => { 1256 + if (isPlaying) pause(); 1257 + }); 1258 + } 1259 + 1260 + // Redraw on parameter changes 1261 + Object.values(inputs).forEach((input) => { 1262 + input.addEventListener("input", () => { 1263 + if (!isPlaying) drawCanvas(); 1264 + }); 1265 + }); 1266 + 1267 + // Initialize 1268 + setupTimeline(); 1269 + drawCanvas(); 1270 + 1271 + // Initial calculation 1272 + calculate(); 1273 + } 1274 + })(); 1275 + </script>
+6 -4
templates/shortcodes/img.html
··· 1 <figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}> 2 - <img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} /> 3 - {% if caption %} 4 - <figcaption>{{caption}}</figcaption> 5 - {% endif %} 6 </figure>
··· 1 <figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}> 2 + <div class="img-container" onclick="openLightbox('{{id}}')"> 3 + <img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} /> 4 + </div> 5 + {% if caption %} 6 + <figcaption>{{caption | markdown | safe}}</figcaption> 7 + {% endif %} 8 </figure>
+14
templates/shortcodes/imgs.html
···
··· 1 + <figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}> 2 + <div class="img-group" data-images="{{id}}" data-alts="{{alt | default(value='')}}"> 3 + {% set images = id | split(pat=",") %} 4 + {% set alts = alt | default(value="") | split(pat=",") %} 5 + {% for image in images %} 6 + <div class="img-container" onclick="openLightboxGroup(this)"> 7 + <img src="{{image | trim}}" {% if alts[loop.index0] %}alt="{{alts[loop.index0] | trim}}" {% endif %} /> 8 + </div> 9 + {% endfor %} 10 + </div> 11 + {% if caption %} 12 + <figcaption>{{caption | markdown | safe}}</figcaption> 13 + {% endif %} 14 + </figure>
+87
templates/shortcodes/is.md
···
··· 1 + <div class="bubble" style="visibility: hidden; opacity: 0;"> 2 + <span><span id="status-wrap"><a href="https://bsky.app/@doing.dunkirk.sh" id="verb-link">Kieran is</a> <i id="status-text"></i></span><span id="time-ago-wrap"><span class="time-dash"> - </span><relative-time id="time-ago" datetime="" threshold="P30D"></relative-time></span></span> 3 + </div> 4 + 5 + <script> 6 + async function resolveDidToPds(did) { 7 + if (did.startsWith("did:plc:")) { 8 + const res = await fetch(`https://plc.directory/${did}`); 9 + const doc = await res.json(); 10 + return doc.service?.find(s => s.id === "#atproto_pds")?.serviceEndpoint; 11 + } else if (did.startsWith("did:web:")) { 12 + const domain = did.slice(8); 13 + const res = await fetch(`https://${domain}/.well-known/did.json`); 14 + const doc = await res.json(); 15 + return doc.service?.find(s => s.id === "#atproto_pds")?.serviceEndpoint; 16 + } 17 + return null; 18 + } 19 + async function fetchAtUriListRecords(atUri) { 20 + const match = atUri.match(/^at:\/\/([^/]+)\/([^/]+)$/); 21 + if (!match) return null; 22 + const [, repo, collection] = match; 23 + const pds = await resolveDidToPds(repo); 24 + if (!pds) return null; 25 + const url = `${pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}`; 26 + const res = await fetch(url); 27 + return res.ok ? res.json() : null; 28 + } 29 + function fetchStatus() { 30 + fetchAtUriListRecords("at://did:plc:krxbvxvis5skq7jj6eot23ul/a.status.update") 31 + .then((statusData) => { 32 + if (!statusData) return; 33 + const bubble = document.querySelector(".bubble"); 34 + if (statusData.records && statusData.records.length > 0) { 35 + if (statusData.records[0].value.createdAt) { 36 + const createdAt = statusData.records[0].value.createdAt; 37 + const createdDate = new Date(createdAt); 38 + const now = new Date(); 39 + const diffInMs = now - createdDate; 40 + const diffInMins = Math.floor(diffInMs / (1000 * 60)); 41 + const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); 42 + if (diffInHours > 12) { 43 + bubble.style.display = "none"; 44 + return; 45 + } 46 + const latestStatus = `"${statusData.records[0].value.text}"`; 47 + document.getElementById("status-text").textContent = latestStatus; 48 + const timeEl = document.getElementById("time-ago"); 49 + timeEl.setAttribute("datetime", createdAt); 50 + const verbLink = document.getElementById("verb-link"); 51 + if (diffInMins > 30) { 52 + verbLink.textContent = "Kieran was"; 53 + } else { 54 + verbLink.textContent = "Kieran is"; 55 + } 56 + bubble.style.display = "block"; 57 + bubble.classList.add("animate-in"); 58 + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 59 + bubble.style.transform = "none"; 60 + bubble.style.opacity = "1"; 61 + } 62 + checkTimeWrap(); 63 + } 64 + } 65 + }) 66 + .catch((error) => { 67 + console.error("Error fetching status update:", error); 68 + }); 69 + } 70 + let resizeTimeout; 71 + function checkTimeWrap() { 72 + const wrap = document.getElementById("time-ago-wrap"); 73 + const statusText = document.getElementById("status-text"); 74 + if (wrap && statusText) { 75 + const wrapTop = wrap.getBoundingClientRect().top; 76 + const statusTop = statusText.getBoundingClientRect().top; 77 + wrap.classList.toggle("wrapped", wrapTop > statusTop); 78 + } 79 + } 80 + function debouncedCheckWrap() { 81 + clearTimeout(resizeTimeout); 82 + resizeTimeout = setTimeout(checkTimeWrap, 100); 83 + } 84 + document.addEventListener("DOMContentLoaded", fetchStatus); 85 + window.addEventListener("resize", debouncedCheckWrap); 86 + setInterval(fetchStatus, 3600000); 87 + </script>
+655
templates/shortcodes/lensDiagram.html
···
··· 1 + <div 2 + id="rayTracer" 3 + style="display: flex; flex-direction: column; min-height: 40rem" 4 + > 5 + <div class="controls" style="display: flex; flex-direction: column"> 6 + <div style="display: flex; gap: 20px; align-items: center"> 7 + <div> 8 + <label>Mirror Type:</label> 9 + <select id="mirrorType"> 10 + <option value="concave">Concave Mirror</option> 11 + <option value="convex">Convex Mirror</option> 12 + </select> 13 + </div> 14 + <div> 15 + <label>Radius of Curvature:</label> 16 + <input 17 + type="number" 18 + id="radius" 19 + value="20" 20 + min="0.2" 21 + step="0.2" 22 + /> 23 + </div> 24 + <div> 25 + <label>Object Distance:</label> 26 + <input 27 + type="number" 28 + id="objectDist" 29 + value="30" 30 + min="0.2" 31 + step="0.2" 32 + /> 33 + </div> 34 + </div> 35 + <div style="display: flex; gap: 20px; align-items: center; width: 100%"> 36 + <div> 37 + <label>Object Height:</label> 38 + <input 39 + type="number" 40 + id="objectHeight" 41 + value="20" 42 + min="0.1" 43 + step="0.1" 44 + /> 45 + </div> 46 + <div style="flex: 1"> 47 + <label>Zoom:</label> 48 + <input 49 + type="range" 50 + id="zoom" 51 + min="0.01" 52 + max="8" 53 + step="0.01" 54 + value="1" 55 + style="width: 100%" 56 + /> 57 + </div> 58 + </div> 59 + </div> 60 + <canvas id="canvas" style="flex: 1; cursor: move"></canvas> 61 + </div> 62 + 63 + <style> 64 + #rayTracer { 65 + padding: 20px; 66 + } 67 + .controls { 68 + margin-bottom: 20px; 69 + } 70 + .controls div { 71 + margin: 0.2rem 0; 72 + } 73 + #canvas { 74 + border: 1px solid #ccc; 75 + width: 100%; 76 + } 77 + </style> 78 + 79 + <script> 80 + const canvas = document.getElementById("canvas"); 81 + const ctx = canvas.getContext("2d"); 82 + const mirrorType = document.getElementById("mirrorType"); 83 + const radiusInput = document.getElementById("radius"); 84 + const objectDistInput = document.getElementById("objectDist"); 85 + const objectHeightInput = document.getElementById("objectHeight"); 86 + const zoomInput = document.getElementById("zoom"); 87 + 88 + let offsetX = 0; 89 + let offsetY = 0; 90 + let isDragging = false; 91 + let lastX = 0; 92 + let lastY = 0; 93 + 94 + canvas.addEventListener("mousedown", (e) => { 95 + isDragging = true; 96 + lastX = e.clientX; 97 + lastY = e.clientY; 98 + }); 99 + 100 + canvas.addEventListener("mousemove", (e) => { 101 + if (isDragging) { 102 + offsetX += e.clientX - lastX; 103 + offsetY += e.clientY - lastY; 104 + lastX = e.clientX; 105 + lastY = e.clientY; 106 + update(); 107 + } 108 + }); 109 + 110 + canvas.addEventListener("mouseup", () => { 111 + isDragging = false; 112 + }); 113 + 114 + canvas.addEventListener("mouseleave", () => { 115 + isDragging = false; 116 + }); 117 + 118 + canvas.addEventListener("wheel", (e) => { 119 + e.preventDefault(); 120 + const zoomSpeed = 0.001; 121 + const newZoom = parseFloat(zoomInput.value) - e.deltaY * zoomSpeed; 122 + zoomInput.value = Math.min(Math.max(newZoom, 0.01), 8); 123 + update(); 124 + }); 125 + 126 + function calculateReflectedRay( 127 + startX, 128 + startY, 129 + incidentX, 130 + incidentY, 131 + centerX, 132 + centerY, 133 + radius, 134 + ) { 135 + // Calculate normal vector at intersection point 136 + const nx = (incidentX - centerX) / radius; 137 + const ny = (incidentY - centerY) / radius; 138 + 139 + // Calculate incident vector 140 + const ix = incidentX - startX; 141 + const iy = incidentY - startY; 142 + const iLen = Math.sqrt(ix * ix + iy * iy); 143 + const dirX = ix / iLen; 144 + const dirY = iy / iLen; 145 + 146 + // Calculate reflection using r = i - 2(iยทn)n 147 + const dot = dirX * nx + dirY * ny; 148 + const reflectX = dirX - 2 * dot * nx; 149 + const reflectY = dirY - 2 * dot * ny; 150 + 151 + // Extend reflected ray to edge of canvas 152 + const t = Math.max( 153 + Math.abs((0 - incidentX) / reflectX), 154 + Math.abs((canvas.width - incidentX) / reflectX), 155 + Math.abs((0 - incidentY) / reflectY), 156 + Math.abs((canvas.height - incidentY) / reflectY), 157 + ); 158 + 159 + return { 160 + x: incidentX + reflectX * t, 161 + y: incidentY + reflectY * t, 162 + }; 163 + } 164 + 165 + function drawMirror(isConcave, R) { 166 + const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value); 167 + const centerX = canvas.width / 2 + R * scale * isConcave + offsetX; 168 + const centerY = canvas.height / 2 + offsetY; 169 + 170 + ctx.beginPath(); 171 + ctx.strokeStyle = "black"; 172 + if (isConcave) { 173 + ctx.arc( 174 + centerX - R * scale, 175 + centerY, 176 + R * scale, 177 + -Math.PI / 2.75, 178 + Math.PI / 2.75, 179 + ); 180 + } else { 181 + ctx.arc( 182 + centerX + R * scale, 183 + centerY, 184 + R * scale, 185 + -Math.PI / 2.75 + Math.PI, 186 + Math.PI / 2.75 + Math.PI, 187 + ); 188 + } 189 + ctx.stroke(); 190 + } 191 + 192 + function drawArrow(x, y, height) { 193 + const arrowHeadSize = height * 0.1; // Scale arrow head with height 194 + ctx.lineWidth = 1; 195 + ctx.beginPath(); 196 + 197 + // Draw the main shaft 198 + ctx.moveTo(x, y); 199 + ctx.lineTo(x, y - height * 0.9); 200 + 201 + // Draw the arrow head 202 + ctx.moveTo(x, y - height); 203 + ctx.lineTo(x - arrowHeadSize, y - height + arrowHeadSize); 204 + ctx.moveTo(x, y - height); 205 + ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize); 206 + ctx.moveTo(x - arrowHeadSize, y - height + arrowHeadSize); 207 + ctx.lineTo(x + arrowHeadSize, y - height + arrowHeadSize); 208 + 209 + ctx.stroke(); 210 + } 211 + 212 + function extendRayToCanvasEdge(x1, y1, x2, y2) { 213 + const rayDirX = x2 - x1; 214 + const rayDirY = y2 - y1; 215 + const t = Math.max( 216 + Math.abs((0 - x2) / rayDirX), 217 + Math.abs((canvas.width - x2) / rayDirX), 218 + Math.abs((0 - y2) / rayDirY), 219 + Math.abs((canvas.height - y2) / rayDirY), 220 + ); 221 + ctx.lineTo(x2 + rayDirX * t, y2 + rayDirY * t); 222 + } 223 + 224 + function findCircleIntersection(radius, x1, h, x3, y3, centerX, centerY) { 225 + // Check if the input values are valid 226 + if (radius <= 0) { 227 + throw new Error("Invalid input values."); 228 + } 229 + 230 + // Calculate the slope of the line from (x1, h) to (x3, y3) 231 + const m = (y3 - (centerY - h)) / (x3 - x1); 232 + 233 + // Define the line equation: y = h + m * (x - x1) 234 + // Substitute into circle equation: (x-centerX)^2 + (y-centerY)^2 = radius^2 235 + // y = h + m * (x - x1) 236 + // (x-centerX)^2 + (h + m*(x-x1) - centerY)^2 = radius^2 237 + 238 + // Coefficients for the quadratic equation 239 + const a = 1 + m * m; 240 + const b = -2 * centerX + 2 * m * (centerY - h - centerY - m * x1); 241 + const c = 242 + centerX * centerX + 243 + (centerY - h - centerY - m * x1) * 244 + (centerY - h - centerY - m * x1) - 245 + radius * radius; 246 + 247 + // Calculate the discriminant 248 + const discriminant = b * b - 4 * a * c; 249 + 250 + if (discriminant < 0) { 251 + throw new Error("No intersection found."); 252 + } 253 + 254 + // Calculate the two possible x values 255 + const xIntersect1 = (-b + Math.sqrt(discriminant)) / (2 * a); 256 + const xIntersect2 = (-b - Math.sqrt(discriminant)) / (2 * a); 257 + 258 + // Calculate the corresponding y values 259 + const yIntersect1 = centerY - h + m * (xIntersect1 - x1); 260 + const yIntersect2 = centerY - h + m * (xIntersect2 - x1); 261 + 262 + // Return the intersection points 263 + return [ 264 + { x: xIntersect1, y: yIntersect1 }, 265 + { x: xIntersect2, y: yIntersect2 }, 266 + ]; 267 + } 268 + 269 + function drawRays(isConcave, R, objDist) { 270 + const scale = (canvas.width / (R * 6)) * parseFloat(zoomInput.value); 271 + const F = R / 2; 272 + const h = parseFloat(objectHeightInput.value) * scale; 273 + const centerX = canvas.width / 2 + R * scale + offsetX; 274 + const centerY = canvas.height / 2 + offsetY; 275 + const objX = 276 + centerX + 277 + objDist * scale * (isConcave ? -1 : -1) - 278 + R * scale * !isConcave; 279 + const objY = centerY; 280 + 281 + drawArrow(objX, objY, h); 282 + 283 + ctx.beginPath(); 284 + ctx.moveTo(0, centerY); 285 + ctx.lineTo(canvas.width, centerY); 286 + ctx.stroke(); 287 + 288 + ctx.fillStyle = "red"; 289 + ctx.beginPath(); 290 + ctx.arc(centerX - F * scale, centerY, 3, 0, 2 * Math.PI); 291 + ctx.fill(); 292 + 293 + ctx.fillStyle = "blue"; 294 + ctx.beginPath(); 295 + ctx.arc(centerX - R * scale * isConcave, centerY, 3, 0, 2 * Math.PI); 296 + ctx.fill(); 297 + 298 + const circleCenterX = isConcave 299 + ? centerX - R * scale 300 + : centerX - R * scale; 301 + 302 + if (isConcave) { 303 + // ray that travels from the top of the object towards the mirror and then calculating the bounce angle it goes in that direction 304 + ctx.strokeStyle = "green"; 305 + ctx.beginPath(); 306 + ctx.lineTo(objX, objY - h); 307 + let intersectionX = 308 + Math.sqrt((R * scale) ** 2 - h ** 2) + circleCenterX; 309 + ctx.lineTo(intersectionX, objY - h); 310 + extendRayToCanvasEdge( 311 + intersectionX, 312 + objY - h, 313 + centerX - F * scale, 314 + centerY, 315 + ); 316 + ctx.stroke(); 317 + 318 + // draw an extension of the ray through the mirror in a slightly opacified color 319 + ctx.strokeStyle = "rgba(0, 128, 0, 0.5)"; 320 + ctx.beginPath(); 321 + ctx.lineTo(intersectionX, objY - h); 322 + extendRayToCanvasEdge( 323 + centerX - F * scale, 324 + centerY, 325 + intersectionX, 326 + objY - h, 327 + ); 328 + ctx.stroke(); 329 + 330 + // draw a point at the intersection of the ray and the mirror 331 + ctx.fillStyle = "black"; 332 + ctx.beginPath(); 333 + ctx.arc(intersectionX, objY - h, 3, 0, 2 * Math.PI); 334 + ctx.fill(); 335 + 336 + // draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror 337 + ctx.strokeStyle = "purple"; 338 + ctx.beginPath(); 339 + ctx.lineTo(objX, objY - h); 340 + ctx.lineTo(centerX - F * scale, centerY); 341 + const extendedRay2 = findCircleIntersection( 342 + R * scale, 343 + objX, 344 + h, 345 + centerX - F * scale, 346 + centerY, 347 + circleCenterX, 348 + centerY, 349 + ); 350 + ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y); 351 + ctx.lineTo(0, extendedRay2[0].y); 352 + ctx.stroke(); 353 + 354 + // draw an extension of the ray through the mirror in a slightly opacified color 355 + ctx.strokeStyle = "rgba(128, 0, 128, 0.5)"; 356 + ctx.beginPath(); 357 + ctx.lineTo(extendedRay2[0].x, extendedRay2[0].y); 358 + ctx.lineTo(canvas.width, extendedRay2[0].y); 359 + ctx.stroke(); 360 + 361 + // draw a point at the intersection of the ray and the mirror 362 + ctx.fillStyle = "black"; 363 + ctx.beginPath(); 364 + ctx.arc(extendedRay2[0].x, extendedRay2[0].y, 3, 0, 2 * Math.PI); 365 + ctx.fill(); 366 + 367 + // draw a ray that travels from the top of the object through the radius of curvature of the mirror 368 + ctx.strokeStyle = "orange"; 369 + ctx.beginPath(); 370 + ctx.lineTo(objX, objY - h); 371 + ctx.lineTo(circleCenterX, centerY); 372 + const extendedRay3 = findCircleIntersection( 373 + R * scale, 374 + objX, 375 + h, 376 + circleCenterX, 377 + centerY, 378 + circleCenterX, 379 + centerY, 380 + ); 381 + ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y); 382 + extendRayToCanvasEdge( 383 + extendedRay3[0].x, 384 + extendedRay3[0].y, 385 + centerX - R * scale, 386 + centerY, 387 + ); 388 + ctx.stroke(); 389 + 390 + // draw an extension of the ray through the mirror in a slightly opacified color 391 + ctx.strokeStyle = "rgba(255, 165, 0, 0.5)"; 392 + ctx.beginPath(); 393 + ctx.lineTo(extendedRay3[0].x, extendedRay3[0].y); 394 + extendRayToCanvasEdge( 395 + centerX - R * scale, 396 + centerY, 397 + extendedRay3[0].x, 398 + extendedRay3[0].y, 399 + ); 400 + ctx.stroke(); 401 + 402 + // draw a point at the intersection of the ray and the mirror 403 + ctx.fillStyle = "black"; 404 + ctx.beginPath(); 405 + ctx.arc(extendedRay3[0].x, extendedRay3[0].y, 3, 0, 2 * Math.PI); 406 + ctx.fill(); 407 + } else { 408 + // draw a ray that travels from the top of the object horizontally towards the mirror 409 + ctx.strokeStyle = "green"; 410 + ctx.beginPath(); 411 + ctx.lineTo(objX, objY - h); 412 + ctx.lineTo( 413 + centerX - Math.sqrt((R * scale) ** 2 - h ** 2), 414 + objY - h, 415 + ); 416 + extendRayToCanvasEdge( 417 + centerX - F * scale, 418 + centerY, 419 + centerX - Math.sqrt((R * scale) ** 2 - h ** 2), 420 + objY - h, 421 + ); 422 + ctx.stroke(); 423 + 424 + // draw an extension of the ray through the mirror in a slightly opacified color 425 + ctx.strokeStyle = "rgba(0, 128, 0, 0.5)"; 426 + ctx.beginPath(); 427 + ctx.lineTo( 428 + centerX - Math.sqrt((R * scale) ** 2 - h ** 2), 429 + objY - h, 430 + ); 431 + ctx.lineTo(centerX - F * scale, centerY); 432 + ctx.stroke(); 433 + 434 + // draw a point at the intersection of the ray and the mirror 435 + ctx.fillStyle = "black"; 436 + ctx.beginPath(); 437 + ctx.arc( 438 + centerX - Math.sqrt((R * scale) ** 2 - h ** 2), 439 + objY - h, 440 + 3, 441 + 0, 442 + 2 * Math.PI, 443 + ); 444 + ctx.fill(); 445 + 446 + // draw a ray that travels from the top of the object towards the focal point of the mirror and through the focal point till it reaches the mirror 447 + ctx.strokeStyle = "purple"; 448 + ctx.beginPath(); 449 + ctx.lineTo(objX, objY - h); 450 + const extendedRay2 = findCircleIntersection( 451 + R * scale, 452 + objX, 453 + h, 454 + centerX - F * scale, 455 + centerY, 456 + circleCenterX, 457 + centerY, 458 + ); 459 + const extendedRay2Y = centerY - (extendedRay2[0].y - centerY); 460 + ctx.lineTo( 461 + centerX - 462 + Math.sqrt( 463 + (R * scale) ** 2 - (centerY - extendedRay2Y) ** 2, 464 + ), 465 + centerY - (extendedRay2[0].y - centerY), 466 + ); 467 + ctx.lineTo(0, centerY - (extendedRay2[0].y - centerY)); 468 + ctx.stroke(); 469 + 470 + // draw an extension of the ray through the mirror in a slightly opacified color 471 + ctx.strokeStyle = "rgba(128, 0, 128, 0.5)"; 472 + ctx.beginPath(); 473 + ctx.lineTo( 474 + centerX - 475 + Math.sqrt( 476 + (R * scale) ** 2 - (centerY - extendedRay2Y) ** 2, 477 + ), 478 + centerY - (extendedRay2[0].y - centerY), 479 + ); 480 + ctx.lineTo(canvas.width, centerY - (extendedRay2[0].y - centerY)); 481 + ctx.stroke(); 482 + 483 + ctx.fillStyle = "black"; 484 + ctx.beginPath(); 485 + ctx.arc( 486 + centerX - 487 + Math.sqrt( 488 + (R * scale) ** 2 - (centerY - extendedRay2Y) ** 2, 489 + ), 490 + centerY - (extendedRay2[0].y - centerY), 491 + 3, 492 + 0, 493 + 2 * Math.PI, 494 + ); 495 + ctx.fill(); 496 + 497 + // draw a ray that travels from the top of the object through the radius of curvature of the mirror 498 + ctx.strokeStyle = "orange"; 499 + ctx.beginPath(); 500 + ctx.lineTo(objX, objY - h); 501 + // ctx.lineTo(centerX, centerY); 502 + const extendedRay3ScaleFactor = 503 + (R * scale) / Math.abs(objX - centerX); 504 + ctx.lineTo( 505 + centerX - 506 + Math.sqrt( 507 + (R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2, 508 + ), 509 + centerY - h * extendedRay3ScaleFactor, 510 + ); 511 + extendRayToCanvasEdge( 512 + centerX - 513 + Math.sqrt( 514 + (R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2, 515 + ), 516 + centerY - h * extendedRay3ScaleFactor, 517 + objX, 518 + objY - h, 519 + ); 520 + ctx.stroke(); 521 + 522 + // draw an extension of the ray through the mirror in a slightly opacified color 523 + ctx.strokeStyle = "rgba(255, 165, 0, 0.5)"; 524 + ctx.beginPath(); 525 + ctx.lineTo( 526 + centerX - 527 + Math.sqrt( 528 + (R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2, 529 + ), 530 + centerY - h * extendedRay3ScaleFactor, 531 + ); 532 + extendRayToCanvasEdge( 533 + centerX - 534 + Math.sqrt( 535 + (R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2, 536 + ), 537 + centerY - h * extendedRay3ScaleFactor, 538 + centerX, 539 + centerY, 540 + ); 541 + ctx.stroke(); 542 + 543 + // draw a point at the intersection of the ray and the mirror 544 + 545 + ctx.fillStyle = "black"; 546 + ctx.beginPath(); 547 + ctx.arc( 548 + centerX - 549 + Math.sqrt( 550 + (R * scale) ** 2 - (h * extendedRay3ScaleFactor) ** 2, 551 + ), 552 + centerY - h * extendedRay3ScaleFactor, 553 + 3, 554 + 0, 555 + 2 * Math.PI, 556 + ); 557 + ctx.fill(); 558 + 559 + // draw an extension of the ray through the mirror in a slightly opacified color 560 + ctx.strokeStyle = "rgba(255, 165, 0, 0.5)"; 561 + ctx.beginPath(); 562 + ctx.lineTo( 563 + centerX - 564 + Math.sqrt( 565 + (R * scale) ** 2 - (centerY - extendedRay3Y) ** 2, 566 + ), 567 + centerY - (extendedRay3[0].y - centerY), 568 + ); 569 + extendRayToCanvasEdge( 570 + centerX - R * scale, 571 + centerY, 572 + centerX - 573 + Math.sqrt( 574 + (R * scale) ** 2 - (centerY - extendedRay3Y) ** 2, 575 + ), 576 + centerY - (extendedRay3[0].y - centerY), 577 + ); 578 + ctx.stroke(); 579 + } 580 + } 581 + 582 + function update() { 583 + canvas.width = canvas.offsetWidth; 584 + canvas.height = canvas.offsetHeight; 585 + ctx.clearRect(0, 0, canvas.width, canvas.height); 586 + ctx.fillStyle = "#f0f0f0"; 587 + ctx.fillRect(0, 0, canvas.width, canvas.height); 588 + const isConcave = mirrorType.value === "concave"; 589 + const R = parseFloat(radiusInput.value); 590 + const objDist = parseFloat(objectDistInput.value); 591 + 592 + drawMirror(isConcave, R); 593 + drawRays(isConcave, R, objDist); 594 + } 595 + 596 + function resetHeight() { 597 + objectHeightInput.value = Math.max( 598 + parseFloat(((radiusInput.value * 2) / 3).toFixed(2)), 599 + 0.1, 600 + ); 601 + } 602 + 603 + mirrorType.addEventListener("change", update); 604 + radiusInput.addEventListener("input", () => { 605 + resetHeight(); 606 + update(); 607 + }); 608 + objectDistInput.addEventListener("input", update); 609 + objectHeightInput.addEventListener("input", update); 610 + zoomInput.addEventListener("input", update); 611 + window.addEventListener("resize", update); 612 + 613 + resetHeight(); 614 + update(); 615 + 616 + let isCanvasHovered = false; 617 + 618 + canvas.addEventListener("mouseenter", () => { 619 + isCanvasHovered = true; 620 + }); 621 + 622 + canvas.addEventListener("mouseleave", () => { 623 + isCanvasHovered = false; 624 + }); 625 + 626 + document.addEventListener("keydown", (e) => { 627 + if (!isCanvasHovered) return; 628 + if (e.key === "+" || e.key === "=") { 629 + zoomInput.value = Math.min(parseFloat(zoomInput.value) + 0.1, 8); 630 + update(); 631 + } 632 + if (e.key === "-" || e.key === "_") { 633 + zoomInput.value = Math.max(parseFloat(zoomInput.value) - 0.1, 0.1); 634 + update(); 635 + } 636 + 637 + // translate the canvas 638 + if (e.key === "ArrowUp") { 639 + offsetY -= 25; 640 + update(); 641 + } 642 + if (e.key === "ArrowDown") { 643 + offsetY += 25; 644 + update(); 645 + } 646 + if (e.key === "ArrowLeft") { 647 + offsetX -= 25; 648 + update(); 649 + } 650 + if (e.key === "ArrowRight") { 651 + offsetX += 25; 652 + update(); 653 + } 654 + }); 655 + </script>
+1 -1
templates/shortcodes/mark.html
··· 1 - <mark>{{content}}</mark>
··· 1 + <mark>{{content}}</mark>
-4
templates/shortcodes/webring.html
··· 1 - <span class="webring"> 2 - <a class="no-style" href={{prev}}><svg class="icons"><use href="{{ get_url(path='icons.svg#chevronLeft', trailing_slash=false) | safe }}"></use></svg></a> 3 - <a href={{webring}}>{{webringName}}</a> 4 - <a class="no-style" href={{next}}><svg class="icons"><use href="{{ get_url(path='icons.svg#chevronRight', trailing_slash=false) | safe }}"></use></svg></a></span>
···
+13 -10
templates/shortcodes/youtube.html
··· 1 - <div class="yt-embed"> 2 - <iframe 3 - src="https://www.youtube-nocookie.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}" 4 - allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 5 - webkitallowfullscreen 6 - mozallowfullscreen 7 - allowfullscreen 8 - > 9 - </iframe> 10 - </div>
··· 1 + <figure class="yt-embed"> 2 + <iframe 3 + src="https://www.youtube-nocookie.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}" 4 + allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 5 + webkitallowfullscreen 6 + mozallowfullscreen 7 + allowfullscreen 8 + > 9 + </iframe> 10 + {% if caption %} 11 + <figcaption>{{caption}}</figcaption> 12 + {% endif %} 13 + </figure>
+5 -7
templates/tags/list.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 <h1>{{ taxonomy.name }}</h1> 5 <p> 6 - {% for term in terms %} 7 - <a href="{{ term.permalink | safe }}">#{{ term.name }}</a>[{{ term.pages | length }}] 8 - {% endfor %} 9 </p> 10 - {% endblock content %}
··· 1 + {% extends "base.html" %} {% block content %} 2 <h1>{{ taxonomy.name }}</h1> 3 <p> 4 + {% for term in terms %} 5 + <a href="{{ term.permalink | safe }}">{{ term.name }}</a>[{{ term.pages | 6 + length }}] {% endfor %} 7 </p> 8 + {% endblock content %}
+20 -16
templates/tags/single.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 <h1>{{ term.name }}</h1> 5 - {% if paginator %} 6 - {% set pages = paginator.pages %} 7 - {% else %} 8 - {% set pages = term.pages %} 9 - {% endif %} 10 <ul> 11 - {% for page in pages %} 12 - <li> 13 - <a href="{{ page.permalink | safe }}">{% if page.date %}{{ page.date }} - {% endif %}{{ page.title }}</a> 14 - </li> 15 - {% endfor %} 16 </ul> 17 {% if paginator %} 18 - <p>{% if paginator.previous %}<a href="{{ paginator.first }}">&lt;&lt; First</a> <a href="{{ paginator.previous }}">&lt; Previous</a>{% endif %} [{{ paginator.current_index }}/{{ paginator.number_pagers }}] {% if paginator.next %}<a href="{{ paginator.next }}">Next &gt;</a> <a href="{{ paginator.last }}">Last &gt;&gt;</a>{% endif %}</p> 19 - {% endif %} 20 - {% endblock content %}
··· 1 + {% extends "base.html" %} {% block content %} 2 <h1>{{ term.name }}</h1> 3 + {% if paginator %} {% set pages = paginator.pages %} {% else %} {% set pages = 4 + term.pages %} {% endif %} 5 <ul> 6 + {% for page in pages %} 7 + <li> 8 + <a href="{{ page.permalink | safe }}" 9 + >{% if page.date %}<relative-time datetime="{{ page.date | date(format='%Y-%m-%dT%H:%M:%S%z') }}" threshold="P30D">{{ page.date | split(pat="T") | first }}</relative-time> - {% endif %}{{ page.title }}</a 10 + > 11 + </li> 12 + {% endfor %} 13 </ul> 14 {% if paginator %} 15 + <p> 16 + {% if paginator.previous %}<a href="{{ paginator.first }}" 17 + >&lt;&lt; First</a 18 + > 19 + <a href="{{ paginator.previous }}">&lt; Previous</a>{% endif %} [{{ 20 + paginator.current_index }}/{{ paginator.number_pagers }}] {% if 21 + paginator.next %}<a href="{{ paginator.next }}">Next &gt;</a> 22 + <a href="{{ paginator.last }}">Last &gt;&gt;</a>{% endif %} 23 + </p> 24 + {% endif %} {% endblock content %}
tools/bun.lockb

This is a binary file and will not be displayed.

-90
tools/genOG.ts
··· 1 - import puppeteer from "puppeteer"; 2 - import { readdir, mkdir } from "node:fs/promises"; 3 - 4 - const template = await Bun.file("tools/og.html").text(); 5 - 6 - const browser = await puppeteer.launch(); 7 - 8 - async function og( 9 - postname: string, 10 - outputPath: string, 11 - width = 1200, 12 - height = 630, 13 - ) { 14 - const page = await browser.newPage(); 15 - 16 - await page.setViewport({ width, height }); 17 - 18 - await page.setContent(template.toString().replace("{{postname}}", postname)); 19 - 20 - await page.screenshot({ path: outputPath }); 21 - } 22 - 23 - async function fileExists(path: string): Promise<boolean> { 24 - try { 25 - await Bun.file(path); 26 - return true; 27 - } catch (e) { 28 - return false; 29 - } 30 - } 31 - 32 - try { 33 - // check if the public/blog folder exists 34 - // if not exit 35 - // if it does, get all the folders and then get the title tag from the index.html 36 - 37 - if (!(await fileExists("public/"))) { 38 - console.error("public/ does not exist"); 39 - process.exit(1); 40 - } 41 - 42 - // read all the files in the current directory filtering for index.htmls 43 - const files = (await readdir("public/", { recursive: true })).filter((file) => 44 - file.endsWith("index.html"), 45 - ); 46 - 47 - const directories = new Set( 48 - files.map((file) => file.replace("index.html", "")), 49 - ); 50 - 51 - const existing = (await readdir("static/")).filter((file) => 52 - directories.has(file), 53 - ); 54 - 55 - // create not existing 56 - for (const dir of directories) { 57 - if (!existing.includes(dir)) { 58 - await mkdir(`static/${dir.split("/").slice(0, -1).join("/")}`, { 59 - recursive: true, 60 - }); 61 - } 62 - } 63 - 64 - console.log("Generating OG images for", files.length, "files"); 65 - 66 - // for each file, get the title tag from the index.html 67 - for (const file of files) { 68 - const index = await Bun.file(`public/${file}`).text(); 69 - let title: string; 70 - if (file.startsWith("tags/")) { 71 - const parts = file.split("/"); 72 - title = `Tag: ${parts[1]}`; // take the next directory as the title 73 - } else { 74 - const match = index.match(/<title>(.*?)<\/title>/); 75 - if (match) { 76 - title = match[1]; 77 - } else { 78 - console.error(`No title found for ${file}`); 79 - continue; 80 - } 81 - } 82 - 83 - console.log("Generating OG for", title); 84 - await og(title, `static/${file.replace("index.html", "og.png")}`); 85 - } 86 - } catch (e) { 87 - console.error(e); 88 - } finally { 89 - await browser.close(); 90 - }
···
-70
tools/og.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <style> 5 - :root, 6 - ::backdrop { 7 - color-scheme: dark; 8 - --bg: #222529; 9 - --bg-light: #464949; 10 - --text: #d6d6d6; 11 - --text-light: #c5c0b7; 12 - --accent: #78b6ad; 13 - --accent-light: #87c9e5; 14 - --accent-text: var(--bg); 15 - --border: #dbd5bc; 16 - --link: #e2c8a2; 17 - } 18 - 19 - body { 20 - font-weight: 600; 21 - color: #d6d6d6; 22 - background-color: #222529; 23 - font-family: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", 24 - monospace; 25 - display: flex; 26 - flex-direction: column; 27 - text-align: center; 28 - } 29 - 30 - div { 31 - margin: 0; 32 - display: flex; 33 - flex-direction: column; 34 - align-items: center; 35 - justify-content: center; 36 - height: 90vh; /* 90% of viewport height */ 37 - width: 90vw; /* 90% of viewport width */ 38 - padding: 5vh 5vw; /* 5% border on all sides */ 39 - box-sizing: border-box; 40 - align-self: center; 41 - } 42 - 43 - h1 { 44 - font-size: calc( 45 - 2rem + 2vw 46 - ); /* Adjust font size based on viewport width */ 47 - margin: 0.5em 0; 48 - line-height: 1.1; 49 - } 50 - 51 - h1::before { 52 - color: var(--accent); 53 - content: "# "; 54 - } 55 - 56 - p { 57 - margin: 1rem 0; 58 - font-size: calc( 59 - 1rem + 1vw 60 - ); /* Adjust font size based on viewport width */ 61 - } 62 - </style> 63 - </head> 64 - <body> 65 - <div> 66 - <h1>{{postname}}</h1> 67 - <p>By Kieran Klukas</p> 68 - </div> 69 - </body> 70 - </html>
···
-15
tools/package.json
··· 1 - { 2 - "name": "zera", 3 - "module": "index.ts", 4 - "type": "module", 5 - "scripts": { 6 - "gen": "bun run tools/genOG.ts" 7 - }, 8 - "devDependencies": { 9 - "@types/bun": "latest", 10 - "puppeteer": "^23.6.0" 11 - }, 12 - "peerDependencies": { 13 - "typescript": "^5.0.0" 14 - } 15 - }
···