diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index bf25b13..5885ba6 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - site: [furbooru, derpibooru] + site: [furbooru, derpibooru, tantabus] steps: - name: Checkout repository @@ -27,10 +27,10 @@ jobs: - name: Build extension for ${{ matrix.site }} run: | - if [ "${{ matrix.site }}" = "derpibooru" ]; then - npm run build:derpibooru - else + if [ "${{ matrix.site }}" = "furbooru" ]; then npm run build + else + npm run build:${{ matrix.site }} fi - name: Create extension zip diff --git a/.vite/pack-extension.js b/.vite/pack-extension.js index d264e9f..a5cf79d 100644 --- a/.vite/pack-extension.js +++ b/.vite/pack-extension.js @@ -67,13 +67,24 @@ export async function packExtension(settings) { return entry; }) - if (process.env.SITE === 'derpibooru') { - manifest.replaceHostTo([ - 'derpibooru.org', - 'trixiebooru.org' - ]); - manifest.replaceBooruNameWith('Derpibooru'); - manifest.setGeckoIdentifier('derpibooru-tagging-assistant@thecore.city'); + switch (process.env.SITE) { + case 'derpibooru': + manifest.replaceHostTo([ + 'derpibooru.org', + 'trixiebooru.org' + ]); + manifest.replaceBooruNameWith('Derpibooru'); + manifest.setGeckoIdentifier('derpibooru-tagging-assistant@thecore.city'); + break; + + case 'tantabus': + manifest.replaceHostTo('tantabus.ai'); + manifest.replaceBooruNameWith('Tantabus'); + manifest.setGeckoIdentifier('tantabus-tagging-assistant@thecore.city'); + break; + + default: + console.warn('No replacement set up for site: ' + process.env.SITE); } manifest.passVersionFromPackage(path.resolve(settings.rootDir, 'package.json')); diff --git a/README.md b/README.md index 23e8eef..ec7e55c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Philomena Tagging Assistant -This is a browser extension written for the [Furbooru](https://furbooru.org) and [Derpibooru](https://derpibooru.org) -image-boards. It gives you the ability to manually go over the list of images and apply tags to them without opening -each individual image. +This is a browser extension written for the [Furbooru](https://furbooru.org), [Derpibooru](https://derpibooru.org) and +[Tantabus](https://tantabus.ai) image-boards. It gives you the ability to manually go over the list of images and apply +tags to them without opening each individual image. ## Installation @@ -59,14 +59,17 @@ npm install --save-dev Second, you need to run the `build` command. It will first build the popup using SvelteKit and then build all the content scripts/stylesheets and copy the manifest afterward. -Extension can currently be built for 2 different imageboards using one of the following commands: +Extension can currently be built for multiple different imageboards using one of the following commands: ```shell -# To build the extension for Furbooru, use: +# Furbooru: npm run build -# To build the extension for Derpbooru, use: +# Derpibooru: npm run build:derpibooru + +# Tantabus: +npm run build:tantabus ``` When build is complete, extension files can be found in the `/build` directory. These files can be either used diff --git a/manifest.json b/manifest.json index c6f48fd..0fd6be3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Furbooru Tagging Assistant", "description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.", - "version": "0.5.4", + "version": "0.6.0", "browser_specific_settings": { "gecko": { "id": "furbooru-tagging-assistant@thecore.city" @@ -69,6 +69,19 @@ "js": [ "src/content/tags.ts" ] + }, + { + "matches": [ + "*://*.furbooru.org/posts", + "*://*.furbooru.org/posts?*", + "*://*.furbooru.org/forums/*/topics/*" + ], + "js": [ + "src/content/posts.ts" + ], + "css": [ + "src/styles/content/posts.scss" + ] } ], "action": { diff --git a/package-lock.json b/package-lock.json index ea755b5..3061692 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,30 @@ { "name": "furbooru-tagging-assistant", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "furbooru-tagging-assistant", - "version": "0.5.4", + "version": "0.6.0", "dependencies": { - "@fortawesome/fontawesome-free": "^7.1.0", + "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.2", + "@sveltejs/kit": "^2.53.0", "amd-lite": "^1.0.1", "lz-string": "^1.5.0", "sass": "^1.97.3", - "svelte": "^5.50.0" + "svelte": "^5.53.3" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@types/chrome": "^0.1.36", - "@types/node": "^25.2.2", + "@types/chrome": "^0.1.37", + "@types/node": "^25.3.0", "@vitest/coverage-v8": "^4.0.18", "cheerio": "^1.2.0", "cross-env": "^10.1.0", - "jsdom": "^28.0.0", - "svelte-check": "^4.3.6", + "jsdom": "^28.1.0", + "svelte-check": "^4.4.3", "typescript": "^5.9.3", "vite": "^7.3.1", "vitest": "^4.0.18" @@ -38,33 +38,26 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", - "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.4" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.6", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", - "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -72,17 +65,7 @@ "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.4" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "lru-cache": "^11.2.6" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -152,10 +135,23 @@ "node": ">=18" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -169,13 +165,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, "funding": [ { @@ -189,17 +185,17 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -213,21 +209,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -241,16 +237,16 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.23", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.23.tgz", - "integrity": "sha512-YEmgyklR6l/oKUltidNVYdjSmLSW88vMsKx0pmiS3r71s8ZZRpd8A0Yf0U+6p/RzElmMnPBv27hNWjDQMSZRtQ==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", + "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", "dev": true, "funding": [ { @@ -262,15 +258,12 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } + "license": "MIT-0" }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -284,7 +277,7 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@epic-web/invariant": { @@ -729,9 +722,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", - "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" @@ -1150,9 +1143,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.50.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.2.tgz", - "integrity": "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA==", + "version": "2.53.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.0.tgz", + "integrity": "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -1160,12 +1153,11 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, @@ -1177,10 +1169,10 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -1240,9 +1232,9 @@ } }, "node_modules/@types/chrome": { - "version": "0.1.36", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.36.tgz", - "integrity": "sha512-BvHbuyGttYXnGt5Gpwa4769KIinKHY1iLjlAPrrMBS2GI9m/XNMPtdsq0NgQalyuUdxvlMN/0OyGw0shFVIoUQ==", + "version": "0.1.37", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.37.tgz", + "integrity": "sha512-IJE4ceuDO7lrEuua7Pow47zwNcI8E6qqkowRP7aFPaZ0lrjxh6y836OPqqkIZeTX64FTogbw+4RNH0+QrweCTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1286,15 +1278,21 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", - "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -1465,9 +1463,10 @@ "license": "MIT" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -1683,31 +1682,21 @@ } }, "node_modules/cssstyle": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", - "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", + "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "@asamuzakjp/css-color": "^5.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" + "lru-cache": "^11.2.6" }, "engines": { "node": ">=20" } }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -1775,9 +1764,9 @@ } }, "node_modules/devalue": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", - "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -2167,16 +2156,17 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz", - "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", "dependencies": { "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.7.6", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", - "cssstyle": "^5.3.7", + "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", @@ -2187,7 +2177,7 @@ "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", - "undici": "^7.20.0", + "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", @@ -2253,6 +2243,16 @@ "version": "3.0.0", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lz-string": { "version": "1.5.0", "license": "MIT", @@ -2318,6 +2318,7 @@ }, "node_modules/mri": { "version": "1.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2563,6 +2564,7 @@ }, "node_modules/sade": { "version": "1.8.1", + "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -2705,20 +2707,21 @@ } }, "node_modules/svelte": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.50.0.tgz", - "integrity": "sha512-FR9kTLmX5i0oyeQ5j/+w8DuagIkQ7MWMuPpPVioW2zx9Dw77q+1ufLzF1IqNtcTXPRnIIio4PlasliVn43OnbQ==", + "version": "5.53.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.3.tgz", + "integrity": "sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -2731,9 +2734,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz", - "integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.3.tgz", + "integrity": "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==", "dev": true, "license": "MIT", "dependencies": { @@ -2950,9 +2953,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, diff --git a/package.json b/package.json index 9263983..13fbbf2 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "furbooru-tagging-assistant", - "version": "0.5.4", + "version": "0.6.0", "private": true, + "type": "module", "scripts": { "build": "npm run build:popup && npm run build:extension", "build:derpibooru": "cross-env SITE=derpibooru npm run build", + "build:tantabus": "cross-env SITE=tantabus npm run build", "build:popup": "vite build", "build:extension": "node build-extension.js", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -12,27 +14,26 @@ "test": "vitest run --coverage", "test:watch": "vitest watch --coverage" }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@types/chrome": "^0.1.36", - "@types/node": "^25.2.2", - "@vitest/coverage-v8": "^4.0.18", - "cheerio": "^1.2.0", - "cross-env": "^10.1.0", - "jsdom": "^28.0.0", - "svelte-check": "^4.3.6", - "typescript": "^5.9.3", - "vite": "^7.3.1", - "vitest": "^4.0.18" - }, - "type": "module", "dependencies": { "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.2", - "@fortawesome/fontawesome-free": "^7.1.0", + "@sveltejs/kit": "^2.53.0", + "@fortawesome/fontawesome-free": "^7.2.0", "amd-lite": "^1.0.1", "lz-string": "^1.5.0", "sass": "^1.97.3", - "svelte": "^5.50.0" + "svelte": "^5.53.3" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/chrome": "^0.1.37", + "@types/node": "^25.3.0", + "@vitest/coverage-v8": "^4.0.18", + "cheerio": "^1.2.0", + "cross-env": "^10.1.0", + "jsdom": "^28.1.0", + "svelte-check": "^4.4.3", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18" } } diff --git a/src/config/tags.ts b/src/config/tags.ts index f60f69a..dfc2897 100644 --- a/src/config/tags.ts +++ b/src/config/tags.ts @@ -14,6 +14,28 @@ export const categories: string[] = [ 'body-type', ]; +/** + * Mapping of namespaces to their respective categories. These namespaces are automatically assigned to them, so we can + * automatically assume categories of tags which start with them. Mapping is extracted from Philomena directly. + * + * This mapping may differ between boorus. + * + * @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/tags/tag.ex#L33-L45 + */ +export const namespaceCategories: Map = new Map([ + ['artist', 'origin'], + ['art pack', 'content-fanmade'], + ['colorist', 'origin'], + ['comic', 'content-fanmade'], + ['editor', 'origin'], + ['fanfic', 'content-fanmade'], + ['oc', 'oc'], + ['photographer', 'origin'], + ['series', 'content-fanmade'], + ['spoiler', 'spoiler'], + ['video', 'content-fanmade'], +]); + /** * List of tags which marked by the site as blacklisted. These tags are blocked from being added by the tag editor and * should usually just be removed automatically. diff --git a/src/content/components/BlockCommunication.ts b/src/content/components/BlockCommunication.ts new file mode 100644 index 0000000..852a80b --- /dev/null +++ b/src/content/components/BlockCommunication.ts @@ -0,0 +1,65 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; +import TagSettings from "$lib/extension/settings/TagSettings"; +import { getComponent } from "$content/components/base/component-utils"; +import { decodeTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/booru/tag-utils"; + +export class BlockCommunication extends BaseComponent { + #contentSection: HTMLElement | null = null; + #tagLinks: HTMLAnchorElement[] = []; + + #tagLinksReplaced: boolean | null = null; + + protected build() { + this.#contentSection = this.container.querySelector('.communication__content'); + this.#tagLinks = this.#findAllTagLinks(); + } + + protected init() { + BlockCommunication.#tagSettings.resolveReplaceLinks().then(this.#onReplaceLinkSettingResolved.bind(this)); + BlockCommunication.#tagSettings.subscribe(settings => { + this.#onReplaceLinkSettingResolved(settings.replaceLinks ?? false); + }); + } + + #onReplaceLinkSettingResolved(haveToReplaceLinks: boolean) { + if (!this.#tagLinks.length || this.#tagLinksReplaced === haveToReplaceLinks) { + return; + } + + for (const linkElement of this.#tagLinks) { + linkElement.classList.toggle('tag', haveToReplaceLinks); + + // Sometimes tags are being decorated with the code block inside. It should be fine to replace it right away. + if (linkElement.childElementCount === 1 && linkElement.children[0].tagName === 'CODE') { + linkElement.textContent = linkElement.children[0].textContent; + } + + if (haveToReplaceLinks) { + const maybeDecodedTagName = decodeTagNameFromLink(linkElement.pathname) ?? ''; + linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(maybeDecodedTagName) ?? ''; + } else { + linkElement.dataset.tagCategory = ''; + } + } + + this.#tagLinksReplaced = haveToReplaceLinks; + } + + #findAllTagLinks(): HTMLAnchorElement[] { + return Array + .from(this.#contentSection?.querySelectorAll('a') || []) + .filter(link => link.pathname.startsWith('/tags/')) + } + + static #tagSettings = new TagSettings(); + + static findAndInitializeAll() { + for (const container of document.querySelectorAll('.block.communication')) { + if (getComponent(container)) { + continue; + } + + new BlockCommunication(container).initialize(); + } + } +} diff --git a/src/content/components/MaintenancePopup.ts b/src/content/components/MaintenancePopup.ts index acdeacb..53f3cd4 100644 --- a/src/content/components/MaintenancePopup.ts +++ b/src/content/components/MaintenancePopup.ts @@ -11,6 +11,7 @@ import { EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events"; import type { MediaBoxTools } from "$content/components/MediaBoxTools"; +import { resolveTagCategoryFromTagName } from "$lib/booru/tag-utils"; class BlackListedTagsEncounteredError extends Error { constructor(tagName: string) { @@ -121,8 +122,13 @@ export class MaintenancePopup extends BaseComponent { // Just to prevent duplication, we need to include this tag to the map of suggested invalid tags if (tagsBlacklist.includes(tagName)) { - MaintenancePopup.#markTagAsInvalid(tagElement); + MaintenancePopup.#markTagElementWithCategory(tagElement, 'error'); this.#suggestedInvalidTags.set(tagName, tagElement); + } else { + MaintenancePopup.#markTagElementWithCategory( + tagElement, + resolveTagCategoryFromTagName(tagName) ?? '', + ); } }); } @@ -287,7 +293,7 @@ export class MaintenancePopup extends BaseComponent { } const tagElement = MaintenancePopup.#buildTagElement(tagName); - MaintenancePopup.#markTagAsInvalid(tagElement); + MaintenancePopup.#markTagElementWithCategory(tagElement, 'error'); tagElement.classList.add('is-present'); this.#suggestedInvalidTags.set(tagName, tagElement); @@ -315,12 +321,13 @@ export class MaintenancePopup extends BaseComponent { } /** - * Marks the tag with red color. + * Mark the tag element with specified category. * @param tagElement Element to mark. + * @param category Code name of category to mark. */ - static #markTagAsInvalid(tagElement: HTMLElement) { - tagElement.dataset.tagCategory = 'error'; - tagElement.setAttribute('data-tag-category', 'error'); + static #markTagElementWithCategory(tagElement: HTMLElement, category: string) { + tagElement.dataset.tagCategory = category; + tagElement.setAttribute('data-tag-category', category); } /** diff --git a/src/content/posts.ts b/src/content/posts.ts new file mode 100644 index 0000000..a331a58 --- /dev/null +++ b/src/content/posts.ts @@ -0,0 +1,3 @@ +import { BlockCommunication } from "$content/components/BlockCommunication"; + +BlockCommunication.findAndInitializeAll(); diff --git a/src/lib/booru/tag-utils.ts b/src/lib/booru/tag-utils.ts index 3270685..c7cb0ae 100644 --- a/src/lib/booru/tag-utils.ts +++ b/src/lib/booru/tag-utils.ts @@ -1,3 +1,5 @@ +import { namespaceCategories } from "$config/tags"; + /** * Build the map containing both real tags and their aliases. * @@ -31,3 +33,52 @@ export function buildTagsAndAliasesMap(realAndAliasedTags: string[], realTags: s return tagsAndAliasesMap; } + +const tagLinkRegExp = /\/tags\/(?[^/?#]+)/; + +/** + * List of encoded characters from Philomena. + * + * @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/slug.ex#L52-L57 + */ +const slugEncodedCharacters: Map = new Map([ + ['-dash-', '-'], + ['-fwslash-', '/'], + ['-bwslash-', '\\'], + ['-colon-', ':'], + ['-dot-', '.'], + ['-plus-', '+'], +]); + +/** + * Decode the tag name from its link path. + * + * @param tagLink Full or partial link to the tag. + * + * @return Tag name or NULL if function is failed to recognize the link as tag-related link. + */ +export function decodeTagNameFromLink(tagLink: string): string | null { + tagLinkRegExp.lastIndex = 0; + + const result = tagLinkRegExp.exec(tagLink); + const encodedTagName = result?.groups?.encodedTagName; + + if (!encodedTagName) { + return null; + } + + return decodeURIComponent(encodedTagName) + .replaceAll(/-[a-z]+-/gi, match => slugEncodedCharacters.get(match) ?? match) + .replaceAll('-', ' '); +} + +/** + * Try to resolve the category from the tag name. + * + * @param tagName Name of the tag. + */ +export function resolveTagCategoryFromTagName(tagName: string): string | null { + const namespace = tagName.split(':')[0]; + + return namespaceCategories.get(namespace) ?? null; +} diff --git a/src/lib/extension/settings/TagSettings.ts b/src/lib/extension/settings/TagSettings.ts index e95f7e6..7c69628 100644 --- a/src/lib/extension/settings/TagSettings.ts +++ b/src/lib/extension/settings/TagSettings.ts @@ -2,6 +2,7 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings"; interface TagSettingsFields { groupSeparation: boolean; + replaceLinks: boolean; } export default class TagSettings extends CacheableSettings { @@ -13,7 +14,15 @@ export default class TagSettings extends CacheableSettings { return this._resolveSetting("groupSeparation", true); } + async resolveReplaceLinks() { + return this._resolveSetting("replaceLinks", false); + } + async setGroupSeparation(value: boolean) { return this._writeSetting("groupSeparation", Boolean(value)); } + + async setReplaceLinks(value: boolean) { + return this._writeSetting("replaceLinks", Boolean(value)); + } } diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 525d24f..b56fcfd 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -11,6 +11,10 @@ if (__CURRENT_SITE__ === 'derpibooru') { currentSiteUrl = 'https://derpibooru.org'; } + + if (__CURRENT_SITE__ === 'tantabus') { + currentSiteUrl = 'https://tantabus.ai'; + } diff --git a/src/routes/preferences/tags/+page.svelte b/src/routes/preferences/tags/+page.svelte index 5e60bc9..958c2bf 100644 --- a/src/routes/preferences/tags/+page.svelte +++ b/src/routes/preferences/tags/+page.svelte @@ -5,7 +5,7 @@ import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance"; - import { shouldSeparateTagGroups } from "$stores/preferences/tag"; + import { shouldReplaceLinksOnForumPosts, shouldSeparateTagGroups } from "$stores/preferences/tag"; import { popupTitle } from "$stores/popup"; $popupTitle = 'Tagging Preferences'; @@ -26,4 +26,9 @@ Enable separation of custom tag groups on the image pages + + + Find and replace links to the tags in the forum posts + + diff --git a/src/stores/preferences/tag.ts b/src/stores/preferences/tag.ts index 70722c9..d7a06cf 100644 --- a/src/stores/preferences/tag.ts +++ b/src/stores/preferences/tag.ts @@ -4,15 +4,24 @@ import TagSettings from "$lib/extension/settings/TagSettings"; const tagSettings = new TagSettings(); export const shouldSeparateTagGroups = writable(false); +export const shouldReplaceLinksOnForumPosts = writable(false); -tagSettings.resolveGroupSeparation() - .then(value => shouldSeparateTagGroups.set(value)) +Promise + .allSettled([ + tagSettings.resolveGroupSeparation().then(value => shouldSeparateTagGroups.set(value)), + tagSettings.resolveReplaceLinks().then(value => shouldReplaceLinksOnForumPosts.set(value)), + ]) .then(() => { shouldSeparateTagGroups.subscribe(value => { void tagSettings.setGroupSeparation(value); }); + shouldReplaceLinksOnForumPosts.subscribe(value => { + void tagSettings.setReplaceLinks(value); + }); + tagSettings.subscribe(settings => { shouldSeparateTagGroups.set(Boolean(settings.groupSeparation)); + shouldReplaceLinksOnForumPosts.set(Boolean(settings.replaceLinks)); }); - }) + }); diff --git a/src/styles/colors.scss b/src/styles/colors.scss index 40e72bb..504933a 100644 --- a/src/styles/colors.scss +++ b/src/styles/colors.scss @@ -95,3 +95,31 @@ $warning-border: #95562c; $input-background: #282e39; $input-border: #575e6b; } + +@if environment.$current-site == 'tantabus' { + $background: #221117; + + $text: #e0e0e0; + $text-gray: #bb90a6; + + $link: #ee157a; + $link-hover: #b099dd; + + $header: #811242; + $header-toolbar: #501e36; + $header-hover-background: #5d0d30; + $header-mobile-link-hover: #995470; + + $footer: #2f1d26; + $footer-text: $text-gray; + + $block-header: $header-toolbar; + $block-border: $header-toolbar; + $block-background: #2f1d26; + $block-background-alternate: #26171e; + + $media-box-border: #573142; + + $input-background: #392833; + $input-border: #6b5764; +} diff --git a/src/styles/content/posts.scss b/src/styles/content/posts.scss new file mode 100644 index 0000000..47df002 --- /dev/null +++ b/src/styles/content/posts.scss @@ -0,0 +1,9 @@ +@use '$styles/booru-vars'; + +.block.communication { + .tag { + &:hover { + color: booru-vars.$resolved-tag-color; + } + } +} diff --git a/src/styles/injectable/tag.scss b/src/styles/injectable/tag.scss index db105d4..8643967 100644 --- a/src/styles/injectable/tag.scss +++ b/src/styles/injectable/tag.scss @@ -9,7 +9,7 @@ padding: 0 4px; display: flex; - @if environment.$current-site == 'derpibooru' { + @if environment.$current-site == 'derpibooru' or environment.$current-site == 'tantabus' { border: 1px solid colors.$tag-border; line-height: 24px; } diff --git a/vite.config.ts b/vite.config.ts index b6d5b66..530213a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,14 @@ export default defineConfig(() => { __CURRENT_SITE_NAME__: JSON.stringify('Derpibooru'), } }), + SwapDefinedVariablesPlugin({ + envVariable: 'SITE', + expectedValue: 'tantabus', + define: { + __CURRENT_SITE__: JSON.stringify('tantabus'), + __CURRENT_SITE_NAME__: JSON.stringify('Tantabus'), + } + }), ], test: { globals: true,