Add qualifyLocalURLsInMarkdown()
This will handle special links in the description and similar fields
This commit is contained in:
parent
c38275e2f2
commit
cd67f6a5c5
20 changed files with 982 additions and 296 deletions
package-lock.jsonpackage.json
src
502
package-lock.json
generated
502
package-lock.json
generated
|
@ -1,27 +1,27 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
"@astrojs/rss": "^4.0.7",
|
"@astrojs/rss": "^4.0.7",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.14",
|
||||||
"astro": "^4.13.1",
|
"astro": "^4.13.3",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.9",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
|
@ -32,8 +32,8 @@
|
||||||
"fetch-retry": "^6.0.0",
|
"fetch-retry": "^6.0.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
"tsx": "^4.16.5"
|
"tsx": "^4.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../astro-pagefind/packages/astro-pagefind": {
|
"../astro-pagefind/packages/astro-pagefind": {
|
||||||
|
@ -936,6 +936,23 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||||
|
@ -1824,9 +1841,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.13",
|
"version": "0.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.14.tgz",
|
||||||
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
|
"integrity": "sha512-ZvOCjUbsJBjL9CxQBn+VEnFpouzuKhxh2dH8xMIWHILL+HfOYtlAkWcyoon8LlzE53d2Yo6YO6pahKKNW3q1YQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash.castarray": "^4.4.0",
|
"lodash.castarray": "^4.4.0",
|
||||||
|
@ -2237,12 +2254,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astro": {
|
"node_modules/astro": {
|
||||||
"version": "4.13.1",
|
"version": "4.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/astro/-/astro-4.13.3.tgz",
|
||||||
"integrity": "sha512-VnMjAc+ykFsIVjgbu9Mt/EA1fMIcPMXbU89h3ATwGOzSIKDsQH72bDgfJkWiwk6u0OE90GeP5EPhAT28Twf9oA==",
|
"integrity": "sha512-MyhmM0v5sphiVwxAm5jjKxWeuPZijWPJ8Ajdign9QzEmLWSH8vUYIJWx/dWRQ6vF1I0jXrksoj3wtw5nzXt9nw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.10.0",
|
"@astrojs/compiler": "^2.10.1",
|
||||||
"@astrojs/internal-helpers": "0.4.1",
|
"@astrojs/internal-helpers": "0.4.1",
|
||||||
"@astrojs/markdown-remark": "5.2.0",
|
"@astrojs/markdown-remark": "5.2.0",
|
||||||
"@astrojs/telemetry": "3.1.0",
|
"@astrojs/telemetry": "3.1.0",
|
||||||
|
@ -2291,7 +2308,7 @@
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"rehype": "^13.0.1",
|
"rehype": "^13.0.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"shiki": "^1.12.0",
|
"shiki": "^1.12.1",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"tsconfck": "^3.1.1",
|
"tsconfck": "^3.1.1",
|
||||||
|
@ -5658,9 +5675,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-tailwindcss": {
|
"node_modules/prettier-plugin-tailwindcss": {
|
||||||
"version": "0.6.5",
|
"version": "0.6.6",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz",
|
||||||
"integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==",
|
"integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -5678,6 +5695,7 @@
|
||||||
"prettier-plugin-import-sort": "*",
|
"prettier-plugin-import-sort": "*",
|
||||||
"prettier-plugin-jsdoc": "*",
|
"prettier-plugin-jsdoc": "*",
|
||||||
"prettier-plugin-marko": "*",
|
"prettier-plugin-marko": "*",
|
||||||
|
"prettier-plugin-multiline-arrays": "*",
|
||||||
"prettier-plugin-organize-attributes": "*",
|
"prettier-plugin-organize-attributes": "*",
|
||||||
"prettier-plugin-organize-imports": "*",
|
"prettier-plugin-organize-imports": "*",
|
||||||
"prettier-plugin-sort-imports": "*",
|
"prettier-plugin-sort-imports": "*",
|
||||||
|
@ -5715,6 +5733,9 @@
|
||||||
"prettier-plugin-marko": {
|
"prettier-plugin-marko": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"prettier-plugin-multiline-arrays": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"prettier-plugin-organize-attributes": {
|
"prettier-plugin-organize-attributes": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
@ -6568,9 +6589,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.7",
|
"version": "3.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
|
||||||
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
|
@ -6790,13 +6811,13 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.16.5",
|
"version": "4.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz",
|
||||||
"integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==",
|
"integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.21.5",
|
"esbuild": "~0.23.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -6809,6 +6830,437 @@
|
||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/esbuild": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.23.0",
|
||||||
|
"@esbuild/android-arm": "0.23.0",
|
||||||
|
"@esbuild/android-arm64": "0.23.0",
|
||||||
|
"@esbuild/android-x64": "0.23.0",
|
||||||
|
"@esbuild/darwin-arm64": "0.23.0",
|
||||||
|
"@esbuild/darwin-x64": "0.23.0",
|
||||||
|
"@esbuild/freebsd-arm64": "0.23.0",
|
||||||
|
"@esbuild/freebsd-x64": "0.23.0",
|
||||||
|
"@esbuild/linux-arm": "0.23.0",
|
||||||
|
"@esbuild/linux-arm64": "0.23.0",
|
||||||
|
"@esbuild/linux-ia32": "0.23.0",
|
||||||
|
"@esbuild/linux-loong64": "0.23.0",
|
||||||
|
"@esbuild/linux-mips64el": "0.23.0",
|
||||||
|
"@esbuild/linux-ppc64": "0.23.0",
|
||||||
|
"@esbuild/linux-riscv64": "0.23.0",
|
||||||
|
"@esbuild/linux-s390x": "0.23.0",
|
||||||
|
"@esbuild/linux-x64": "0.23.0",
|
||||||
|
"@esbuild/netbsd-x64": "0.23.0",
|
||||||
|
"@esbuild/openbsd-arm64": "0.23.0",
|
||||||
|
"@esbuild/openbsd-x64": "0.23.0",
|
||||||
|
"@esbuild/sunos-x64": "0.23.0",
|
||||||
|
"@esbuild/win32-arm64": "0.23.0",
|
||||||
|
"@esbuild/win32-ia32": "0.23.0",
|
||||||
|
"@esbuild/win32-x64": "0.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "2.19.0",
|
"version": "2.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "astro sync",
|
"postinstall": "astro sync",
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
|
@ -19,15 +19,15 @@
|
||||||
"@astrojs/rss": "^4.0.7",
|
"@astrojs/rss": "^4.0.7",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.14",
|
||||||
"astro": "^4.13.1",
|
"astro": "^4.13.3",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.9",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"fetch-retry": "^6.0.0",
|
"fetch-retry": "^6.0.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
"tsx": "^4.16.5"
|
"tsx": "^4.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,15 +18,13 @@ const charactersPerUser = copyrightedCharacters ? await formatCopyrightedCharact
|
||||||
charactersPerUser ? (
|
charactersPerUser ? (
|
||||||
<section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
|
<section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
|
||||||
<ul>
|
<ul>
|
||||||
{charactersPerUser.map(([owner, characterList]) => (
|
{charactersPerUser.map(({ user, characters }) => (
|
||||||
<CopyrightedCharactersItem
|
<CopyrightedCharactersItem
|
||||||
stringFunction={
|
stringFunction={(user) =>
|
||||||
characterList[0] === ""
|
t(lang, "characters/characters_are_copyrighted_by", user, characters[0] === "" ? [] : characters)
|
||||||
? (user) => t(lang, "characters/all_characters_are_copyrighted_by", user)
|
|
||||||
: (user) => t(lang, "characters/characters_are_copyrighted_by", user, characterList)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserComponent lang={lang} user={owner} />
|
<UserComponent lang={lang} user={user} />
|
||||||
</CopyrightedCharactersItem>
|
</CopyrightedCharactersItem>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -217,10 +217,11 @@ const publishedContent = z.object({
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
export type Lang = z.output<typeof lang>;
|
export type Lang = z.output<typeof lang>;
|
||||||
export type Website = keyof z.input<typeof websiteLinks>;
|
export type UserWebsite = keyof z.input<typeof websiteLinks>;
|
||||||
export type GamePlatform = z.infer<typeof platform>;
|
export type GamePlatform = z.infer<typeof platform>;
|
||||||
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
||||||
export type PublishedContent = z.infer<typeof publishedContent>;
|
export type PublishedContent = z.infer<typeof publishedContent>;
|
||||||
|
export type PostWebsite = keyof PublishedContent["posts"];
|
||||||
|
|
||||||
// Content collections
|
// Content collections
|
||||||
|
|
||||||
|
@ -305,7 +306,7 @@ const seriesCollection = defineCollection({
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
// Required parameters
|
// Required parameters
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
|
link: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"link" must be a local URL`),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
name: The Lost of the Marshes
|
name: The Lost of the Marshes
|
||||||
url: /stories/the-lost-of-the-marshes
|
link: /stories/the-lost-of-the-marshes
|
||||||
|
|
|
@ -35,6 +35,7 @@ tags:
|
||||||
- nudity
|
- nudity
|
||||||
- masturbation
|
- masturbation
|
||||||
- plushie
|
- plushie
|
||||||
|
- wardrobe malfunction
|
||||||
- commission
|
- commission
|
||||||
commissioner: dee-lumeni
|
commissioner: dee-lumeni
|
||||||
copyrightedCharacters:
|
copyrightedCharacters:
|
||||||
|
|
|
@ -23,3 +23,5 @@ tags:
|
||||||
description: Scenarios where sexual consent is dubious.
|
description: Scenarios where sexual consent is dubious.
|
||||||
- name: plushie
|
- name: plushie
|
||||||
description: Scenarios with sexual action involving stuffed toys or plushie-like characters.
|
description: Scenarios with sexual action involving stuffed toys or plushie-like characters.
|
||||||
|
- name: wardrobe malfunction
|
||||||
|
description: Scenarios where a mishap happens with someone's clothes, such as accidentally revealing body parts and/or bursting open.
|
||||||
|
|
|
@ -238,12 +238,11 @@ const UI_STRINGS = {
|
||||||
},
|
},
|
||||||
"characters/characters_are_copyrighted_by": {
|
"characters/characters_are_copyrighted_by": {
|
||||||
en: (owner: string, charactersList: string[]) =>
|
en: (owner: string, charactersList: string[]) =>
|
||||||
charactersList.length == 1
|
charactersList.length == 0
|
||||||
? `${charactersList[0]} is © ${owner}`
|
? `All characters are © ${owner}`
|
||||||
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
: charactersList.length == 1
|
||||||
},
|
? `${charactersList[0]} is © ${owner}`
|
||||||
"characters/all_characters_are_copyrighted_by": {
|
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
||||||
en: (owner: string) => `All characters are © ${owner}`,
|
|
||||||
},
|
},
|
||||||
// Tag-related strings
|
// Tag-related strings
|
||||||
"tag/total_works_with_tag": {
|
"tag/total_works_with_tag": {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
|
||||||
import Prose from "../components/Prose.astro";
|
import Prose from "../components/Prose.astro";
|
||||||
import MastodonComments from "../components/MastodonComments.astro";
|
import MastodonComments from "../components/MastodonComments.astro";
|
||||||
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
|
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
|
||||||
|
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
|
||||||
|
|
||||||
interface RelatedContent {
|
interface RelatedContent {
|
||||||
link: string;
|
link: string;
|
||||||
|
@ -62,14 +63,16 @@ const categorizedTags = Object.fromEntries(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const description = await qualifyLocalURLsInMarkdown(props.description, props.lang);
|
||||||
|
const summary = props.summary && (await qualifyLocalURLsInMarkdown(props.summary, props.lang));
|
||||||
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
||||||
const tagSlug = slug(tag);
|
const tagSlug = slug(tag);
|
||||||
if (!(tag in categorizedTags)) {
|
if (!(tag in categorizedTags)) {
|
||||||
console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`);
|
console.warn(`WARNING: Tag "${tag}" doesn't have a category in the "tag-categories" collection`);
|
||||||
return { id: tagSlug, name: tag };
|
return { id: tagSlug, name: tag };
|
||||||
}
|
}
|
||||||
if (categorizedTags[tag] == null) {
|
if (categorizedTags[tag] == null) {
|
||||||
console.warn(`No "${props.lang}" translation for tag "${tag}"`);
|
console.warn(`WARNING: No "${props.lang}" translation for tag "${tag}"`);
|
||||||
return { id: tagSlug, name: tag };
|
return { id: tagSlug, name: tag };
|
||||||
}
|
}
|
||||||
return { id: tagSlug, name: categorizedTags[tag] };
|
return { id: tagSlug, name: categorizedTags[tag] };
|
||||||
|
@ -111,7 +114,7 @@ const thumbnail =
|
||||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={series ? series.data.url : props.labelReturnTo.link}
|
href={series ? series.data.link : props.labelReturnTo.link}
|
||||||
class="text-link my-1 p-2"
|
class="text-link my-1 p-2"
|
||||||
aria-labelled-by="label-return-to"
|
aria-labelled-by="label-return-to"
|
||||||
>
|
>
|
||||||
|
@ -291,12 +294,12 @@ const thumbnail =
|
||||||
{t(props.lang, "published_content/description")}
|
{t(props.lang, "published_content/description")}
|
||||||
</h2>
|
</h2>
|
||||||
<Prose>
|
<Prose>
|
||||||
<Markdown of={props.description} />
|
<Markdown of={description} />
|
||||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||||
</Prose>
|
</Prose>
|
||||||
</section>
|
</section>
|
||||||
{
|
{
|
||||||
props.summary ? (
|
summary ? (
|
||||||
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
||||||
<h2 id="title-summary" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-summary" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{t(props.lang, "published_content/summary")}
|
{t(props.lang, "published_content/summary")}
|
||||||
|
@ -307,7 +310,7 @@ const thumbnail =
|
||||||
</summary>
|
</summary>
|
||||||
<div class="px-2 py-1">
|
<div class="px-2 py-1">
|
||||||
<Prose>
|
<Prose>
|
||||||
<Markdown of={props.summary} />
|
<Markdown of={summary} />
|
||||||
</Prose>
|
</Prose>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
@ -417,10 +420,13 @@ const thumbnail =
|
||||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{t(props.lang, "published_content/tags")}
|
{t(props.lang, "published_content/tags")}
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
<ul class="flex flex-wrap gap-x-2 gap-y-3 px-3">
|
||||||
{tags.map(({ id, name }) => (
|
{tags.map(({ id, name }) => (
|
||||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white print:bg-none">
|
<li>
|
||||||
<a class="hover:underline focus:underline" href={`/tags/${id}`}>
|
<a
|
||||||
|
class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm hover:underline focus:underline dark:bg-bm-600 dark:text-white print:bg-none"
|
||||||
|
href={`/tags/${id}`}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import type { APIRoute, GetStaticPaths } from "astro";
|
import type { APIRoute, GetStaticPaths } from "astro";
|
||||||
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
||||||
import type { Website } from "../../../content/config";
|
import type { PostWebsite } from "../../../content/config";
|
||||||
import { t, type Lang } from "../../../i18n";
|
import { t } from "../../../i18n";
|
||||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||||
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||||
import { getUsernameForLang } from "../../../utils/get_username_for_lang";
|
import { getUsernameForLang } from "../../../utils/get_username_for_lang";
|
||||||
import { isAnonymousUser } from "../../../utils/is_anonymous_user";
|
import { isAnonymousUser } from "../../../utils/is_anonymous_user";
|
||||||
|
import { qualifyLocalURLsInMarkdown } from "../../../utils/qualify_local_urls_in_markdown";
|
||||||
|
import { getWebsiteLinkForUser } from "../../../utils/get_website_link_for_user";
|
||||||
|
|
||||||
interface ExportWebsiteInfo {
|
interface ExportWebsiteInfo {
|
||||||
website: Website;
|
website: PostWebsite;
|
||||||
exportFormat: "bbcode" | "markdown";
|
exportFormat: "bbcode" | "markdown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,76 +24,6 @@ const WEBSITE_LIST = [
|
||||||
|
|
||||||
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
||||||
|
|
||||||
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
|
||||||
const link = user.data.links[website];
|
|
||||||
if (link && "username" in link && link.username) {
|
|
||||||
return link.username;
|
|
||||||
}
|
|
||||||
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): boolean {
|
|
||||||
const { preferredLink } = user.data;
|
|
||||||
return !preferredLink || preferredLink == website;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName, lang: Lang): string {
|
|
||||||
const { links, preferredLink } = user.data;
|
|
||||||
switch (website) {
|
|
||||||
case "eka":
|
|
||||||
if ("eka" in links) {
|
|
||||||
return `:icon${getUsernameForWebsite(user, "eka")}:`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "furaffinity":
|
|
||||||
if ("furaffinity" in links) {
|
|
||||||
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "weasyl":
|
|
||||||
if ("weasyl" in links) {
|
|
||||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
|
||||||
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
|
||||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
|
||||||
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
|
|
||||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
|
||||||
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "inkbunny":
|
|
||||||
if ("inkbunny" in links) {
|
|
||||||
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
|
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
|
||||||
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
|
|
||||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
|
||||||
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
|
|
||||||
} else if (isPreferredWebsite(user, "weasyl")) {
|
|
||||||
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "sofurry":
|
|
||||||
if ("sofurry" in links) {
|
|
||||||
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
|
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
|
||||||
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
|
|
||||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
|
||||||
return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
const unknown: never = website;
|
|
||||||
throw new Error(`Unhandled export website "${unknown}"`);
|
|
||||||
}
|
|
||||||
if (preferredLink) {
|
|
||||||
const preferred = links[preferredLink]!;
|
|
||||||
return `[${getUsernameForLang(user, lang)}](${preferred.link})`;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
story: CollectionEntry<"stories">;
|
story: CollectionEntry<"stories">;
|
||||||
};
|
};
|
||||||
|
@ -111,18 +43,21 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
|
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
|
||||||
const { lang } = story.data;
|
try {
|
||||||
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
|
const { lang } = story.data;
|
||||||
const authorsList = await getEntries(story.data.authors);
|
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
|
||||||
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
|
const authorsList = await getEntries(story.data.authors);
|
||||||
const requestersList = story.data.requester && (await getEntries(story.data.requester));
|
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
|
||||||
|
const requestersList = story.data.requester && (await getEntries(story.data.requester));
|
||||||
|
|
||||||
const description = Object.fromEntries(
|
const description = await Promise.all(
|
||||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
WEBSITE_LIST.map(async ({ website, exportFormat }) => {
|
||||||
const u = (user: CollectionEntry<"users">) =>
|
const exportWebsite: ExportWebsiteName = website;
|
||||||
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website, lang);
|
const u = (user: CollectionEntry<"users">) =>
|
||||||
const storyDescription = (
|
isAnonymousUser(user)
|
||||||
[
|
? getUsernameForLang(user, lang)
|
||||||
|
: getWebsiteLinkForUser(user, exportWebsite, (user) => getUsernameForLang(user, lang));
|
||||||
|
const storyDescription = await [
|
||||||
story.data.description,
|
story.data.description,
|
||||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
||||||
t(
|
t(
|
||||||
|
@ -142,62 +77,76 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
"export_story/commissioned_by",
|
"export_story/commissioned_by",
|
||||||
commissionersList.map((commissioner) => u(commissioner)),
|
commissionersList.map((commissioner) => u(commissioner)),
|
||||||
),
|
),
|
||||||
...copyrightedCharacters.map(([user, characterList]) =>
|
...copyrightedCharacters.map(({ user, characters }) =>
|
||||||
characterList[0] == ""
|
t(lang, "characters/characters_are_copyrighted_by", u(user), characters[0] == "" ? [] : characters),
|
||||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
|
||||||
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
|
||||||
),
|
),
|
||||||
].filter((data) => data) as string[]
|
].reduce(async (promise, data) => {
|
||||||
)
|
if (!data) {
|
||||||
.join("\n\n")
|
return promise;
|
||||||
.replaceAll(
|
}
|
||||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
const acc = await promise;
|
||||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
const newData = await qualifyLocalURLsInMarkdown(data, lang, site, exportWebsite);
|
||||||
);
|
return acc ? `${acc}\n\n${newData}` : newData;
|
||||||
switch (exportFormat) {
|
}, Promise.resolve(""));
|
||||||
case "bbcode":
|
switch (exportFormat) {
|
||||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
case "bbcode":
|
||||||
case "markdown":
|
return { exportWebsite, description: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n") };
|
||||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
case "markdown":
|
||||||
default:
|
return { exportWebsite, description: storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim() };
|
||||||
const unknown: never = exportFormat;
|
default:
|
||||||
throw new Error(`Unknown export format "${unknown}"`);
|
const unknown: never = exportFormat;
|
||||||
}
|
throw new Error(`Unknown export format "${unknown}"`);
|
||||||
}),
|
}
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const storyHeader =
|
const storyHeader =
|
||||||
`${story.data.title}\n` +
|
`${story.data.title}\n` +
|
||||||
`${t(
|
`${t(
|
||||||
lang,
|
lang,
|
||||||
"story/authors",
|
"story/authors",
|
||||||
authorsList.map((author) => getUsernameForLang(author, lang)),
|
authorsList.map((author) => getUsernameForLang(author, lang)),
|
||||||
)}\n` +
|
)}\n` +
|
||||||
(requestersList
|
(requestersList
|
||||||
? `${t(
|
? `${t(
|
||||||
lang,
|
lang,
|
||||||
"story/requested_by",
|
"story/requested_by",
|
||||||
requestersList.map((requester) => getUsernameForLang(requester, lang)),
|
requestersList.map((requester) => getUsernameForLang(requester, lang)),
|
||||||
)}\n`
|
)}\n`
|
||||||
: "") +
|
: "") +
|
||||||
(commissionersList
|
(commissionersList
|
||||||
? `${t(
|
? `${t(
|
||||||
lang,
|
lang,
|
||||||
"story/commissioned_by",
|
"story/commissioned_by",
|
||||||
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
|
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
|
||||||
)}\n`
|
)}\n`
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
|
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
|
||||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
story: storyText,
|
story: storyText,
|
||||||
description,
|
description: description.reduce(
|
||||||
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
(acc, item) => {
|
||||||
}),
|
acc[item.exportWebsite] = item.description;
|
||||||
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
return acc;
|
||||||
);
|
},
|
||||||
|
{} as Record<PostWebsite, string>,
|
||||||
|
),
|
||||||
|
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
message: (e as Error).message ?? null,
|
||||||
|
stack: (e as Error).stack ?? null,
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import sanitizeHtml from "sanitize-html";
|
||||||
import { t, type Lang } from "../i18n";
|
import { t, type Lang } from "../i18n";
|
||||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||||
|
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
|
||||||
|
|
||||||
type FeedItem = RSSFeedItem &
|
type FeedItem = RSSFeedItem &
|
||||||
Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>;
|
Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>;
|
||||||
|
@ -28,6 +29,81 @@ const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
|
||||||
return userName;
|
return userName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function storyFeedItem(
|
||||||
|
site: URL | undefined,
|
||||||
|
data: EntryWithPubDate<"stories">["data"],
|
||||||
|
slug: CollectionEntry<"stories">["slug"],
|
||||||
|
body: string,
|
||||||
|
): Promise<FeedItem> {
|
||||||
|
return {
|
||||||
|
title: `New story! "${data.title}"`,
|
||||||
|
pubDate: toNoonUTCDate(data.pubDate),
|
||||||
|
link: `/stories/${slug}`,
|
||||||
|
description:
|
||||||
|
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll(
|
||||||
|
/[\n ]+/g,
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
categories: ["story"],
|
||||||
|
content: sanitizeHtml(
|
||||||
|
`<h1>${data.title}</h1>` +
|
||||||
|
`<p>${t(
|
||||||
|
data.lang,
|
||||||
|
"story/authors",
|
||||||
|
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||||
|
)}</p>` +
|
||||||
|
(data.requester
|
||||||
|
? `<p>${t(
|
||||||
|
data.lang,
|
||||||
|
"story/requested_by",
|
||||||
|
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
|
||||||
|
)}</p>`
|
||||||
|
: "") +
|
||||||
|
(data.commissioner
|
||||||
|
? `<p>${t(
|
||||||
|
data.lang,
|
||||||
|
"story/commissioned_by",
|
||||||
|
(await getEntries(data.commissioner)).map((commissioner) => getLinkForUser(commissioner, data.lang)),
|
||||||
|
)}</p>`
|
||||||
|
: "") +
|
||||||
|
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
|
||||||
|
`<hr>${await markdown(body)}` +
|
||||||
|
`<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gameFeedItem(
|
||||||
|
site: URL | undefined,
|
||||||
|
data: EntryWithPubDate<"games">["data"],
|
||||||
|
slug: CollectionEntry<"games">["slug"],
|
||||||
|
body: string,
|
||||||
|
): Promise<FeedItem> {
|
||||||
|
return {
|
||||||
|
title: `New game! "${data.title}"`,
|
||||||
|
pubDate: toNoonUTCDate(data.pubDate),
|
||||||
|
link: `/games/${slug}`,
|
||||||
|
description:
|
||||||
|
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll(
|
||||||
|
/[\n ]+/g,
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
categories: ["game"],
|
||||||
|
content: sanitizeHtml(
|
||||||
|
`<h1>${data.title}</h1>` +
|
||||||
|
`<p>${t(
|
||||||
|
data.lang,
|
||||||
|
"story/authors",
|
||||||
|
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||||
|
)}</p>` +
|
||||||
|
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||||
|
`<hr><p><em>${data.contentWarning}</em></p>` +
|
||||||
|
`<hr>${await markdown(body)}` +
|
||||||
|
`<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ site }) => {
|
export const GET: APIRoute = async ({ site }) => {
|
||||||
const stories = (
|
const stories = (
|
||||||
(await getCollection(
|
(await getCollection(
|
||||||
|
@ -47,75 +123,21 @@ export const GET: APIRoute = async ({ site }) => {
|
||||||
title: "Gallery | Bad Manners",
|
title: "Gallery | Bad Manners",
|
||||||
description: "Stories, games, and (possibly) more by Bad Manners",
|
description: "Stories, games, and (possibly) more by Bad Manners",
|
||||||
site: site!,
|
site: site!,
|
||||||
items: [
|
items: await Promise.all(
|
||||||
await Promise.all(
|
[
|
||||||
stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
stories.map(({ data, slug, body }) => ({
|
||||||
title: `New story! "${data.title}"`,
|
date: data.pubDate,
|
||||||
pubDate: toNoonUTCDate(data.pubDate),
|
fn: () => storyFeedItem(site, data, slug, body),
|
||||||
link: `/stories/${slug}`,
|
|
||||||
description:
|
|
||||||
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
|
||||||
/[\n ]+/g,
|
|
||||||
" ",
|
|
||||||
),
|
|
||||||
categories: ["story"],
|
|
||||||
content: sanitizeHtml(
|
|
||||||
`<h1>${data.title}</h1>` +
|
|
||||||
`<p>${t(
|
|
||||||
data.lang,
|
|
||||||
"story/authors",
|
|
||||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
|
||||||
)}</p>` +
|
|
||||||
(data.requester
|
|
||||||
? `<p>${t(
|
|
||||||
data.lang,
|
|
||||||
"story/requested_by",
|
|
||||||
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
|
|
||||||
)}</p>`
|
|
||||||
: "") +
|
|
||||||
(data.commissioner
|
|
||||||
? `<p>${t(
|
|
||||||
data.lang,
|
|
||||||
"story/commissioned_by",
|
|
||||||
(await getEntries(data.commissioner)).map((commissioner) =>
|
|
||||||
getLinkForUser(commissioner, data.lang),
|
|
||||||
),
|
|
||||||
)}</p>`
|
|
||||||
: "") +
|
|
||||||
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
|
|
||||||
`<hr>${await markdown(body)}` +
|
|
||||||
`<hr>${await markdown(data.description)}`,
|
|
||||||
),
|
|
||||||
})),
|
})),
|
||||||
),
|
games.map(({ data, slug, body }) => ({
|
||||||
await Promise.all(
|
date: data.pubDate,
|
||||||
games.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
fn: () => gameFeedItem(site, data, slug, body),
|
||||||
title: `New game! "${data.title}"`,
|
|
||||||
pubDate: toNoonUTCDate(data.pubDate),
|
|
||||||
link: `/games/${slug}`,
|
|
||||||
description:
|
|
||||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
|
||||||
/[\n ]+/g,
|
|
||||||
" ",
|
|
||||||
),
|
|
||||||
categories: ["game"],
|
|
||||||
content: sanitizeHtml(
|
|
||||||
`<h1>${data.title}</h1>` +
|
|
||||||
`<p>${t(
|
|
||||||
data.lang,
|
|
||||||
"story/authors",
|
|
||||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
|
||||||
)}</p>` +
|
|
||||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
|
||||||
`<hr><p><em>${data.contentWarning}</em></p>` +
|
|
||||||
`<hr>${await markdown(body)}` +
|
|
||||||
`<hr>${await markdown(data.description)}`,
|
|
||||||
),
|
|
||||||
})),
|
})),
|
||||||
),
|
]
|
||||||
]
|
.flat()
|
||||||
.flat()
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||||
.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime())
|
.slice(0, MAX_ITEMS)
|
||||||
.slice(0, MAX_ITEMS),
|
.map((value) => value.fn()),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,8 @@ const story = Astro.props;
|
||||||
const readingTime = getReadingTime(story.body);
|
const readingTime = getReadingTime(story.body);
|
||||||
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
|
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
`WARNING: "wordCount" differs greatly from actual word count for published story ` +
|
||||||
|
`${story.data.title} ("${story.slug}") ` +
|
||||||
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
import type { GetStaticPaths, Page } from "astro";
|
import type { GetStaticPaths, Page } from "astro";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection, type CollectionEntry } from "astro:content";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
|
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
|
||||||
|
@ -51,7 +50,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
||||||
href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)}
|
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)}
|
||||||
>
|
>
|
||||||
{p + 1}
|
{p + 1}
|
||||||
</a>
|
</a>
|
||||||
|
@ -114,7 +113,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
||||||
href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)}
|
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)}
|
||||||
>
|
>
|
||||||
{p + 1}
|
{p + 1}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
||||||
<meta
|
<meta
|
||||||
slot="head-description"
|
slot="head-description"
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Bad Manners || The story of Quince, Nikili, and Suu."
|
content="The Lost of the Marshes || The story of Quince, Nikili, and Suu."
|
||||||
/>
|
/>
|
||||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
|
||||||
<p class="my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p>
|
<p class="my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p>
|
||||||
|
|
|
@ -64,7 +64,7 @@ const categorizedTags = tagCategories
|
||||||
|
|
||||||
if (uncategorizedTagsSet.size > 0) {
|
if (uncategorizedTagsSet.size > 0) {
|
||||||
const tagList = [...uncategorizedTagsSet];
|
const tagList = [...uncategorizedTagsSet];
|
||||||
console.warn("The following tags have no category:", tagList);
|
console.warn("WARNING: The following tags have no category:", tagList);
|
||||||
// categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
// categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
@ -83,7 +83,7 @@ if (uncategorizedTagsSet.size > 0) {
|
||||||
{
|
{
|
||||||
seriesCollection.map((series) => (
|
seriesCollection.map((series) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="text-link underline" href={series.data.url}>
|
<a class="text-link underline" href={series.data.link}>
|
||||||
{series.data.name}
|
{series.data.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -98,10 +98,14 @@ if (uncategorizedTagsSet.size > 0) {
|
||||||
<h2 id={`category-${categoryId}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id={`category-${categoryId}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{category}
|
{category}
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-3 px-3">
|
||||||
{tagList.map(({ id, name, description }) => (
|
{tagList.map(({ id, name, description }) => (
|
||||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
<li>
|
||||||
<a class="hover:underline focus:underline" href={`/tags/${id}`} title={description}>
|
<a
|
||||||
|
class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm hover:underline focus:underline dark:bg-bm-600 dark:text-white"
|
||||||
|
href={`/tags/${id}`}
|
||||||
|
title={description}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { Markdown } from "@astropub/md";
|
||||||
import { slug } from "github-slugger";
|
import { slug } from "github-slugger";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
import Prose from "../../components/Prose.astro";
|
import Prose from "../../components/Prose.astro";
|
||||||
import { t } from "../../i18n";
|
import { t, DEFAULT_LANG } from "../../i18n";
|
||||||
import { DEFAULT_LANG } from "../../i18n";
|
import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown";
|
||||||
|
|
||||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
category.data.tags.forEach(({ name, description, related }) => {
|
category.data.tags.forEach(({ name, description, related }) => {
|
||||||
related = related.filter((relatedTag) => {
|
related = related.filter((relatedTag) => {
|
||||||
if (relatedTag == name) {
|
if (relatedTag == name) {
|
||||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
console.warn(`WARNING: Tag "${name}" should not have itself as a related tag; removing...`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!tags.has(relatedTag)) {
|
if (!tags.has(relatedTag)) {
|
||||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
console.warn(`WARNING: Tag "${name}" has an unknown related tag "${relatedTag}"; removing...`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -88,23 +88,34 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tag, description, stories, games, related } = Astro.props;
|
const { props } = Astro;
|
||||||
|
const description = props.description && (await qualifyLocalURLsInMarkdown(props.description, DEFAULT_LANG));
|
||||||
if (!description) {
|
if (!description) {
|
||||||
console.warn(`Tag "${tag}" has no description`);
|
console.warn(`WARNING: Tag "${props.tag}" has no description`);
|
||||||
}
|
}
|
||||||
const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stories.length, games.length);
|
const totalWorksWithTag = t(
|
||||||
|
DEFAULT_LANG,
|
||||||
|
"tag/total_works_with_tag",
|
||||||
|
props.tag,
|
||||||
|
props.stories.length,
|
||||||
|
props.games.length,
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
|
||||||
<meta slot="head-description" content={`Bad Manners || ${totalWorksWithTag || tag}`} property="og:description" />
|
<meta
|
||||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
slot="head-description"
|
||||||
|
content={`Bad Manners || ${totalWorksWithTag || props.tag}`}
|
||||||
|
property="og:description"
|
||||||
|
/>
|
||||||
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1>
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<Prose>
|
<Prose>
|
||||||
{description ? <Markdown of={description} /> : null}
|
{description ? <Markdown of={description} /> : null}
|
||||||
{
|
{
|
||||||
related?.length ? (
|
props.related?.length ? (
|
||||||
<p
|
<p
|
||||||
set:html={`See also: ${related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
set:html={`See also: ${props.related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -112,13 +123,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
||||||
</Prose>
|
</Prose>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
stories.length > 0 && (
|
props.stories.length > 0 && (
|
||||||
<section class="my-2" aria-labelledby="content-stories">
|
<section class="my-2" aria-labelledby="content-stories">
|
||||||
<h2 id="content-stories" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="content-stories" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Stories
|
Stories
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{stories.map((story) => (
|
{props.stories.map((story) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a
|
<a
|
||||||
class="text-link hover:underline focus:underline"
|
class="text-link hover:underline focus:underline"
|
||||||
|
@ -135,7 +146,17 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
<div class="max-w-[192px] text-sm">
|
||||||
|
<span>{story.data.title}</span>
|
||||||
|
<br />
|
||||||
|
<span class="italic">
|
||||||
|
{story.data.pubDate.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -144,13 +165,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
games.length > 0 && (
|
props.games.length > 0 && (
|
||||||
<section class="my-2" aria-labelledby="content-games">
|
<section class="my-2" aria-labelledby="content-games">
|
||||||
<h2 id="content-games" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="content-games" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Games
|
Games
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{games.map((game) => (
|
{props.games.map((game) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a
|
<a
|
||||||
class="text-link hover:underline focus:underline"
|
class="text-link hover:underline focus:underline"
|
||||||
|
@ -167,7 +188,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-48 text-sm">{game.data.title}</div>
|
<div class="max-w-[192px] text-sm">
|
||||||
|
<span>{game.data.title}</span>
|
||||||
|
<br />
|
||||||
|
<span class="italic">
|
||||||
|
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -7,14 +7,15 @@ export async function formatCopyrightedCharacters(copyrightedCharacters: Copyrig
|
||||||
Object.keys(copyrightedCharacters).reduce(
|
Object.keys(copyrightedCharacters).reduce(
|
||||||
(acc, character) => {
|
(acc, character) => {
|
||||||
const user = copyrightedCharacters[character];
|
const user = copyrightedCharacters[character];
|
||||||
if (!(user.id in acc)) {
|
if (user.id in acc) {
|
||||||
acc[user.id] = [getEntry(user), []];
|
acc[user.id][1].push(character);
|
||||||
|
} else {
|
||||||
|
acc[user.id] = [getEntry(user), [character]];
|
||||||
}
|
}
|
||||||
acc[user.id][1].push(character);
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, [Promise<CollectionEntry<"users">>, string[]]>,
|
{} as Record<CollectionEntry<"users">["id"], [Promise<CollectionEntry<"users">>, string[]]>,
|
||||||
),
|
),
|
||||||
).map(async ([userPromise, characters]) => [await userPromise, characters] as [CollectionEntry<"users">, string[]]),
|
).map(async ([userPromise, characters]) => ({ user: await userPromise, characters })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang):
|
||||||
throw new Error(`No "${lang}" translation for user "${user.id}"`);
|
throw new Error(`No "${lang}" translation for user "${user.id}"`);
|
||||||
}
|
}
|
||||||
if (lang !== DEFAULT_LANG) {
|
if (lang !== DEFAULT_LANG) {
|
||||||
console.warn(`Name "${name}" for user "${user.id}" isn't translated to "${lang}"; using default`);
|
console.warn(`WARNING: Name "${name}" for user "${user.id}" isn't translated to "${lang}"; using default...`);
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
114
src/utils/get_website_link_for_user.ts
Normal file
114
src/utils/get_website_link_for_user.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { DEFAULT_LANG, type UserWebsite } from "../content/config";
|
||||||
|
import { getUsernameForLang } from "./get_username_for_lang";
|
||||||
|
|
||||||
|
type WebsiteUserData<W extends UserWebsite> = CollectionEntry<"users">["data"]["links"][W];
|
||||||
|
|
||||||
|
function getUserDataForWebsite<W extends UserWebsite>(user: CollectionEntry<"users">, website: W) {
|
||||||
|
const link: WebsiteUserData<W> | undefined = user.data.links[website];
|
||||||
|
if (link) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
throw new Error(`Cannot get "${website}" data for user "${user.id}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPreferredWebsite = (user: CollectionEntry<"users">, website: UserWebsite) =>
|
||||||
|
!!user.data.preferredLink && user.data.preferredLink == website;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a user to the special string that references the user in a specified website -
|
||||||
|
* for example, by adding the `@` in front of a Twitter handle.
|
||||||
|
* Otherwise, it will be formatted as a Markdown link to the user's preferred link.
|
||||||
|
* @param user User to format.
|
||||||
|
* @param website Which website to format with.
|
||||||
|
* The function will attempt to match the user's link for the given website,
|
||||||
|
* or try to use any special formatting rules corresponding with the user's preferred link.
|
||||||
|
* @param usernameFn Optional function that labels the user's preferred link.
|
||||||
|
* By default, this will be a function that returns the username in English.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getWebsiteLinkForUser(
|
||||||
|
user: CollectionEntry<"users">,
|
||||||
|
website: UserWebsite | undefined,
|
||||||
|
usernameFn: (user: CollectionEntry<"users">) => string = (user) => getUsernameForLang(user, DEFAULT_LANG),
|
||||||
|
): string {
|
||||||
|
if (website === "website") {
|
||||||
|
website = undefined;
|
||||||
|
}
|
||||||
|
const { links, preferredLink } = user.data;
|
||||||
|
switch (website) {
|
||||||
|
case undefined:
|
||||||
|
break;
|
||||||
|
case "eka":
|
||||||
|
if ("eka" in links) {
|
||||||
|
return `:icon${getUserDataForWebsite(user, "eka").username}:`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "furaffinity":
|
||||||
|
if ("furaffinity" in links) {
|
||||||
|
return `:icon${getUserDataForWebsite(user, "furaffinity").username}:`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "weasyl":
|
||||||
|
if ("weasyl" in links) {
|
||||||
|
return `<!~${getUserDataForWebsite(user, "weasyl").username.replaceAll(" ", "")}>`;
|
||||||
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
|
return `<fa:${getUserDataForWebsite(user, "furaffinity").username.replaceAll("_", "")}>`;
|
||||||
|
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||||
|
return `<ib:${getUserDataForWebsite(user, "inkbunny").username}>`;
|
||||||
|
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||||
|
return `<sf:${getUserDataForWebsite(user, "sofurry").username}>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "inkbunny":
|
||||||
|
if ("inkbunny" in links) {
|
||||||
|
return `[iconname]${getUserDataForWebsite(user, "inkbunny").username}[/iconname]`;
|
||||||
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
|
return `[fa]${getUserDataForWebsite(user, "furaffinity").username}[/fa]`;
|
||||||
|
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||||
|
return `[sf]${getUserDataForWebsite(user, "sofurry").username}[/sf]`;
|
||||||
|
} else if (isPreferredWebsite(user, "weasyl")) {
|
||||||
|
return `[weasyl]${getUserDataForWebsite(user, "weasyl").username}[/weasyl]`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "sofurry":
|
||||||
|
if ("sofurry" in links) {
|
||||||
|
return `:icon${getUserDataForWebsite(user, "sofurry").username}:`;
|
||||||
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
|
return `fa!${getUserDataForWebsite(user, "furaffinity").username}`;
|
||||||
|
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||||
|
return `ib!${getUserDataForWebsite(user, "inkbunny").username}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "twitter":
|
||||||
|
if ("twitter" in links) {
|
||||||
|
return `@${getUserDataForWebsite(user, "twitter").username}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "mastodon":
|
||||||
|
if ("mastodon" in links) {
|
||||||
|
const { handle, instance } = getUserDataForWebsite(user, "mastodon");
|
||||||
|
return `@${handle}@${instance}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "bluesky":
|
||||||
|
if ("bluesky" in links) {
|
||||||
|
return `@${getUserDataForWebsite(user, "bluesky").username}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "itaku":
|
||||||
|
if ("itaku" in links) {
|
||||||
|
return `@${getUserDataForWebsite(user, "itaku").username}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const unknown: never = website;
|
||||||
|
throw new Error(`Unhandled export website "${unknown}"`);
|
||||||
|
}
|
||||||
|
if (preferredLink) {
|
||||||
|
return `[${usernameFn(user)}](${links[preferredLink]!.link})`;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`No matching${website ? ` "${website}" ` : " "}link for user "${user.id}". (hint: consider setting their "preferredLink" property)`,
|
||||||
|
);
|
||||||
|
}
|
110
src/utils/qualify_local_urls_in_markdown.ts
Normal file
110
src/utils/qualify_local_urls_in_markdown.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { getCollection, getEntry } from "astro:content";
|
||||||
|
import type { Lang, PostWebsite } from "../content/config";
|
||||||
|
import { getWebsiteLinkForUser } from "./get_website_link_for_user";
|
||||||
|
import { getUsernameForLang } from "./get_username_for_lang";
|
||||||
|
|
||||||
|
type MatchGroups =
|
||||||
|
| {
|
||||||
|
text: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
text: string;
|
||||||
|
link: string;
|
||||||
|
contentPrefix: "stories" | "games" | "users";
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOCAL_URL_REGEX =
|
||||||
|
/\[(?<text>[^\]]*)\]\((?<link>(?:\/(?<contentPrefix>stories|games|users)(?!\/?\)|\/[1-9]\d*\/?\))\/(?<slug>[^\)]+))|(?:\/[^\)]+?))\/?\)/g;
|
||||||
|
|
||||||
|
const SERIES_URLS_REGEX_PROMISE = getCollection("series").then(
|
||||||
|
(series) => new RegExp(`^(${series.map(({ data }) => data.link.replace(/\/$/, "")).join("|")})\/?$`),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a Markdown text, finds any local URLs (eg. `[click me](/stories/foo)`) and replaces them with the appropriate
|
||||||
|
* fully qualified link (eg. `[click me](https://example.com/stories/foo)`).
|
||||||
|
* @param originalText The Markdown text to modify.
|
||||||
|
* @param lang The language for translating usernames.
|
||||||
|
* @param site The base URL of the gallery, to append to the start of local links and permalinks.
|
||||||
|
* If undefined, this function will only replace `/users/...` links, while still validating the others.
|
||||||
|
* @param website Which website the Markdown text will be rendered to. If defined, any content links
|
||||||
|
* (stories, games, etc.) will be replaced by the appropriate link on that website whenever possible.
|
||||||
|
* If undefined, this function will only replace `/users/...` links (and permalinks if `site` is set),
|
||||||
|
* while still validating the others.
|
||||||
|
*/
|
||||||
|
export async function qualifyLocalURLsInMarkdown(originalText: string, lang: Lang, site?: URL, website?: PostWebsite) {
|
||||||
|
const replacements: string[] = [];
|
||||||
|
for (const match of originalText.matchAll(new RegExp(LOCAL_URL_REGEX))) {
|
||||||
|
const groups = match.groups as MatchGroups | undefined;
|
||||||
|
if (groups?.text === undefined || groups?.link === undefined) {
|
||||||
|
throw new Error(`Cannot qualify invalid local URL ${match[0]}`);
|
||||||
|
}
|
||||||
|
const { text, link } = groups;
|
||||||
|
// Any links that match a series `link` are handled separately.
|
||||||
|
if ((await SERIES_URLS_REGEX_PROMISE).test(link)) {
|
||||||
|
replacements.push(site ? `[${text}](${new URL(link, site).toString()})` : match[0]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check if this is a special link (story, game, or user)
|
||||||
|
if ("contentPrefix" in groups && groups.contentPrefix) {
|
||||||
|
const { contentPrefix, slug } = groups;
|
||||||
|
switch (contentPrefix) {
|
||||||
|
case "stories":
|
||||||
|
const story = await getEntry("stories", slug);
|
||||||
|
if (!story) {
|
||||||
|
throw new Error(`Couldn't find story with slug "${slug}"`);
|
||||||
|
}
|
||||||
|
if (typeof website === "string" && story.data.posts[website]?.link) {
|
||||||
|
replacements.push(`[${text}](${story.data.posts[website].link})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "games":
|
||||||
|
const game = await getEntry("games", slug);
|
||||||
|
if (!game) {
|
||||||
|
throw new Error(`Couldn't find game with slug "${slug}"`);
|
||||||
|
}
|
||||||
|
if (typeof website === "string" && game.data.posts[website]?.link) {
|
||||||
|
replacements.push(`[${text}](${game.data.posts[website].link})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "users":
|
||||||
|
const user = await getEntry("users", slug);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`Couldn't find user with id "${slug}"`);
|
||||||
|
}
|
||||||
|
// If there's a label in the link, use that if possible
|
||||||
|
if (text) {
|
||||||
|
if (typeof website === "string" && user.data.links[website]?.link) {
|
||||||
|
replacements.push(`[${text}](${user.data.links[website].link})`);
|
||||||
|
continue;
|
||||||
|
} else if (user.data.preferredLink) {
|
||||||
|
replacements.push(`[${text}](${user.data.links[user.data.preferredLink]!.link})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise (i.e. label is empty), use the username for the website
|
||||||
|
replacements.push(getWebsiteLinkForUser(user, website, (user) => getUsernameForLang(user, lang)));
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
const unknown: never = contentPrefix;
|
||||||
|
console.warn(`WARNING: Unknown local link qualifier ${unknown}; ignoring...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacements.push(website && site ? `[${text}](${new URL(link, site).toString()})` : match[0]);
|
||||||
|
}
|
||||||
|
const newText = originalText.replaceAll(new RegExp(LOCAL_URL_REGEX), () => {
|
||||||
|
const replacement = replacements.shift();
|
||||||
|
if (!replacement) {
|
||||||
|
throw new Error(`Replacements array length didn't match length of regex matches! (too few elements)`);
|
||||||
|
}
|
||||||
|
return replacement;
|
||||||
|
});
|
||||||
|
if (replacements.length > 0) {
|
||||||
|
throw new Error(`Replacements array length didn't match length of regex matches! (too many elements)`);
|
||||||
|
}
|
||||||
|
return newText;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue