From 2bdb7897775690d460d1cae65181e9967c7b35e8 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 25 Feb 2026 20:47:13 +0400 Subject: [PATCH 1/6] Support links to `/search?q=` when detecting tag links to replace --- src/content/components/BlockCommunication.ts | 15 +++++-- src/lib/booru/tag-utils.ts | 43 +++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/content/components/BlockCommunication.ts b/src/content/components/BlockCommunication.ts index 852a80b..217170d 100644 --- a/src/content/components/BlockCommunication.ts +++ b/src/content/components/BlockCommunication.ts @@ -1,7 +1,7 @@ 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"; +import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/booru/tag-utils"; export class BlockCommunication extends BaseComponent { #contentSection: HTMLElement | null = null; @@ -35,8 +35,8 @@ export class BlockCommunication extends BaseComponent { } if (haveToReplaceLinks) { - const maybeDecodedTagName = decodeTagNameFromLink(linkElement.pathname) ?? ''; - linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(maybeDecodedTagName) ?? ''; + const tagName = resolveTagNameFromLink(new URL(linkElement.href)) ?? ''; + linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(tagName) ?? ''; } else { linkElement.dataset.tagCategory = ''; } @@ -48,7 +48,14 @@ export class BlockCommunication extends BaseComponent { #findAllTagLinks(): HTMLAnchorElement[] { return Array .from(this.#contentSection?.querySelectorAll('a') || []) - .filter(link => link.pathname.startsWith('/tags/')) + .filter( + link => + // Support links pointing to the tag page. + link.pathname.startsWith('/tags/') + // Also capture link which point to the search results with single tag. + || link.pathname.startsWith('/search') + && link.search.includes('q=') + ); } static #tagSettings = new TagSettings(); diff --git a/src/lib/booru/tag-utils.ts b/src/lib/booru/tag-utils.ts index c7cb0ae..ca110b7 100644 --- a/src/lib/booru/tag-utils.ts +++ b/src/lib/booru/tag-utils.ts @@ -1,4 +1,5 @@ import { namespaceCategories } from "$config/tags"; +import { QueryLexer, QuotedTermToken, TermToken } from "$lib/booru/search/QueryLexer"; /** * Build the map containing both real tags and their aliases. @@ -51,16 +52,48 @@ const slugEncodedCharacters: Map = new Map([ ]); /** - * Decode the tag name from its link path. + * Try to parse the tag name from the search query URL. It uses the same tokenizer as the booru. It only returns the + * tag name if query contains only one single tag without any additional conditions. * - * @param tagLink Full or partial link to the tag. + * @param searchLink Link with search query. * - * @return Tag name or NULL if function is failed to recognize the link as tag-related link. + * @return Tag name or NULL if query contains more than 1 tag or doesn't have any tags at all. */ -export function decodeTagNameFromLink(tagLink: string): string | null { +function parseTagNameFromSearchQuery(searchLink: URL): string | null { + const lexer = new QueryLexer(searchLink.searchParams.get('q') || ''); + const parsedQuery = lexer.parse(); + + if (parsedQuery.length !== 1) { + return null; + } + + const [token] = parsedQuery; + + switch (true) { + case token instanceof TermToken: + return token.value; + case token instanceof QuotedTermToken: + return token.decodedValue; + } + + return null; +} + +/** + * Decode the tag name from the following link. + * + * @param tagLink Search link or link to the tag to parse the tag name from. + * + * @return Tag name or NULL if function is failed to parse the name of the tag. + */ +export function resolveTagNameFromLink(tagLink: URL): string | null { + if (tagLink.pathname.startsWith('/search') && tagLink.searchParams.has('q')) { + return parseTagNameFromSearchQuery(tagLink); + } + tagLinkRegExp.lastIndex = 0; - const result = tagLinkRegExp.exec(tagLink); + const result = tagLinkRegExp.exec(tagLink.pathname); const encodedTagName = result?.groups?.encodedTagName; if (!encodedTagName) { From 5123b573200f976889bac882418fbccf7dd61b91 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 26 Feb 2026 02:33:15 +0400 Subject: [PATCH 2/6] Fixed content scripts not properly receiving Tantabus constant --- .vite/lib/content-scripts.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.vite/lib/content-scripts.js b/.vite/lib/content-scripts.js index 35b12a6..88baf7b 100644 --- a/.vite/lib/content-scripts.js +++ b/.vite/lib/content-scripts.js @@ -174,6 +174,15 @@ export async function buildScriptsAndStyles(buildOptions) { } }); + const tantabusSwapPlugin = SwapDefinedVariablesPlugin({ + envVariable: 'SITE', + expectedValue: 'tantabus', + define: { + __CURRENT_SITE__: JSON.stringify('tantabus'), + __CURRENT_SITE_NAME__: JSON.stringify('Tantabus'), + } + }); + // Building all scripts together with AMD loader in mind await build({ configFile: false, @@ -209,6 +218,7 @@ export async function buildScriptsAndStyles(buildOptions) { ?.push(...dependencies); }), derpibooruSwapPlugin, + tantabusSwapPlugin, ], define: defineConstants, }); @@ -235,6 +245,7 @@ export async function buildScriptsAndStyles(buildOptions) { wrapScriptIntoIIFE(), ScssViteReadEnvVariableFunctionPlugin(), derpibooruSwapPlugin, + tantabusSwapPlugin, ], define: defineConstants, }); From a2ab0d4e7c6ca112e40aedcad645522339b93010 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 26 Feb 2026 02:34:06 +0400 Subject: [PATCH 3/6] Adding 3 more namespaces unique to Tantabus --- src/config/tags.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/tags.ts b/src/config/tags.ts index dfc2897..2065e88 100644 --- a/src/config/tags.ts +++ b/src/config/tags.ts @@ -34,6 +34,11 @@ export const namespaceCategories: Map = new Map([ ['series', 'content-fanmade'], ['spoiler', 'spoiler'], ['video', 'content-fanmade'], + ...(__CURRENT_SITE__ === 'tantabus' ? [ + ["prompter", "origin"], + ["creator", "origin"], + ["generator", "origin"] + ] : []) ]); /** From 8194a84ef7254fbf926cd0fe5780dd9f3b00d12d Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 26 Feb 2026 03:28:48 +0400 Subject: [PATCH 4/6] Fix pluses not being decoded from the path --- src/lib/booru/tag-utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/booru/tag-utils.ts b/src/lib/booru/tag-utils.ts index ca110b7..228c7a3 100644 --- a/src/lib/booru/tag-utils.ts +++ b/src/lib/booru/tag-utils.ts @@ -102,7 +102,8 @@ export function resolveTagNameFromLink(tagLink: URL): string | null { return decodeURIComponent(encodedTagName) .replaceAll(/-[a-z]+-/gi, match => slugEncodedCharacters.get(match) ?? match) - .replaceAll('-', ' '); + .replaceAll('-', ' ') + .replaceAll('+', ' '); } /** From 9031055ec9bfa45969347ba4641be9d79fb99e06 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 26 Feb 2026 03:30:10 +0400 Subject: [PATCH 5/6] Replace the tag link text to resolved tag name when possible --- src/content/components/BlockCommunication.ts | 71 ++++++++++++++++++-- src/lib/extension/settings/TagSettings.ts | 9 +++ src/routes/preferences/tags/+page.svelte | 13 +++- src/stores/preferences/tag.ts | 7 ++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/content/components/BlockCommunication.ts b/src/content/components/BlockCommunication.ts index 217170d..0c0d151 100644 --- a/src/content/components/BlockCommunication.ts +++ b/src/content/components/BlockCommunication.ts @@ -8,6 +8,7 @@ export class BlockCommunication extends BaseComponent { #tagLinks: HTMLAnchorElement[] = []; #tagLinksReplaced: boolean | null = null; + #linkTextReplaced: boolean | null = null; protected build() { this.#contentSection = this.container.querySelector('.communication__content'); @@ -15,14 +16,30 @@ export class BlockCommunication extends BaseComponent { } protected init() { - BlockCommunication.#tagSettings.resolveReplaceLinks().then(this.#onReplaceLinkSettingResolved.bind(this)); + Promise.all([ + BlockCommunication.#tagSettings.resolveReplaceLinks(), + BlockCommunication.#tagSettings.resolveReplaceLinkText(), + ]).then(([replaceLinks, replaceLinkText]) => { + this.#onReplaceLinkSettingResolved( + replaceLinks, + replaceLinkText + ); + }); + BlockCommunication.#tagSettings.subscribe(settings => { - this.#onReplaceLinkSettingResolved(settings.replaceLinks ?? false); + this.#onReplaceLinkSettingResolved( + settings.replaceLinks ?? false, + settings.replaceLinkText ?? true + ); }); } - #onReplaceLinkSettingResolved(haveToReplaceLinks: boolean) { - if (!this.#tagLinks.length || this.#tagLinksReplaced === haveToReplaceLinks) { + #onReplaceLinkSettingResolved(haveToReplaceLinks: boolean, shouldReplaceLinkText: boolean) { + if ( + !this.#tagLinks.length + || this.#tagLinksReplaced === haveToReplaceLinks + && this.#linkTextReplaced === shouldReplaceLinkText + ) { return; } @@ -34,15 +51,52 @@ export class BlockCommunication extends BaseComponent { linkElement.textContent = linkElement.children[0].textContent; } + /** + * Resolved tag name. It should be stored for the text replacement. + */ + let tagName: string | undefined; + if (haveToReplaceLinks) { - const tagName = resolveTagNameFromLink(new URL(linkElement.href)) ?? ''; + tagName = resolveTagNameFromLink(new URL(linkElement.href)) ?? ''; linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(tagName) ?? ''; } else { linkElement.dataset.tagCategory = ''; } + + this.#toggleTagLinkText( + linkElement, + haveToReplaceLinks && shouldReplaceLinkText, + tagName, + ); } this.#tagLinksReplaced = haveToReplaceLinks; + this.#linkTextReplaced = shouldReplaceLinkText; + } + + /** + * Swap the link text with the tag name or restore it back to original content. This function will only perform + * replacement on links without any additional tags inside. This will ensure link won't break original content. + * @param linkElement Element to swap the text on. + * @param shouldSwapToTagName Should we swap the text to tag name or retore it back from memory. + * @param tagName Tag name to swap the text to. If not provided, text will be swapped back. + * @private + */ + #toggleTagLinkText(linkElement: HTMLElement, shouldSwapToTagName: boolean, tagName?: string) { + if (linkElement.childElementCount) { + return; + } + + // Make sure we save the original text to memory. + if (!BlockCommunication.#originalTagLinkTexts.has(linkElement)) { + BlockCommunication.#originalTagLinkTexts.set(linkElement, linkElement.textContent); + } + + if (shouldSwapToTagName && tagName) { + linkElement.textContent = tagName; + } else { + linkElement.textContent = BlockCommunication.#originalTagLinkTexts.get(linkElement) ?? linkElement.textContent; + } } #findAllTagLinks(): HTMLAnchorElement[] { @@ -60,6 +114,13 @@ export class BlockCommunication extends BaseComponent { static #tagSettings = new TagSettings(); + /** + * Map of links to their original texts. These texts need to be stored here to make them restorable. Keys is a link + * element and value is a text. + * @private + */ + static #originalTagLinkTexts: WeakMap = new WeakMap(); + static findAndInitializeAll() { for (const container of document.querySelectorAll('.block.communication')) { if (getComponent(container)) { diff --git a/src/lib/extension/settings/TagSettings.ts b/src/lib/extension/settings/TagSettings.ts index 7c69628..f657557 100644 --- a/src/lib/extension/settings/TagSettings.ts +++ b/src/lib/extension/settings/TagSettings.ts @@ -3,6 +3,7 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings"; interface TagSettingsFields { groupSeparation: boolean; replaceLinks: boolean; + replaceLinkText: boolean; } export default class TagSettings extends CacheableSettings { @@ -18,6 +19,10 @@ export default class TagSettings extends CacheableSettings { return this._resolveSetting("replaceLinks", false); } + async resolveReplaceLinkText() { + return this._resolveSetting("replaceLinkText", true); + } + async setGroupSeparation(value: boolean) { return this._writeSetting("groupSeparation", Boolean(value)); } @@ -25,4 +30,8 @@ export default class TagSettings extends CacheableSettings { async setReplaceLinks(value: boolean) { return this._writeSetting("replaceLinks", Boolean(value)); } + + async setReplaceLinkText(value: boolean) { + return this._writeSetting("replaceLinkText", Boolean(value)); + } } diff --git a/src/routes/preferences/tags/+page.svelte b/src/routes/preferences/tags/+page.svelte index 958c2bf..a9e4df2 100644 --- a/src/routes/preferences/tags/+page.svelte +++ b/src/routes/preferences/tags/+page.svelte @@ -5,7 +5,11 @@ import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance"; - import { shouldReplaceLinksOnForumPosts, shouldSeparateTagGroups } from "$stores/preferences/tag"; + import { + shouldReplaceLinksOnForumPosts, + shouldReplaceTextOfTagLinks, + shouldSeparateTagGroups + } from "$stores/preferences/tag"; import { popupTitle } from "$stores/popup"; $popupTitle = 'Tagging Preferences'; @@ -31,4 +35,11 @@ Find and replace links to the tags in the forum posts + {#if $shouldReplaceLinksOnForumPosts} + + + Try to replace text on links pointing to tags in forum posts + + + {/if} diff --git a/src/stores/preferences/tag.ts b/src/stores/preferences/tag.ts index d7a06cf..f491c5c 100644 --- a/src/stores/preferences/tag.ts +++ b/src/stores/preferences/tag.ts @@ -5,11 +5,13 @@ const tagSettings = new TagSettings(); export const shouldSeparateTagGroups = writable(false); export const shouldReplaceLinksOnForumPosts = writable(false); +export const shouldReplaceTextOfTagLinks = writable(true); Promise .allSettled([ tagSettings.resolveGroupSeparation().then(value => shouldSeparateTagGroups.set(value)), tagSettings.resolveReplaceLinks().then(value => shouldReplaceLinksOnForumPosts.set(value)), + tagSettings.resolveReplaceLinkText().then(value => shouldReplaceTextOfTagLinks.set(value)), ]) .then(() => { shouldSeparateTagGroups.subscribe(value => { @@ -20,8 +22,13 @@ Promise void tagSettings.setReplaceLinks(value); }); + shouldReplaceTextOfTagLinks.subscribe(value => { + void tagSettings.setReplaceLinkText(value); + }); + tagSettings.subscribe(settings => { shouldSeparateTagGroups.set(Boolean(settings.groupSeparation)); shouldReplaceLinksOnForumPosts.set(Boolean(settings.replaceLinks)); + shouldReplaceTextOfTagLinks.set(Boolean(settings.replaceLinkText)); }); }); From d11cc2a9c57241f564478bfc954931b7dc07e20d Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 26 Feb 2026 04:27:01 +0400 Subject: [PATCH 6/6] Bumped version to 0.6.1 --- manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index f9e4949..492076b 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.6.0.1", + "version": "0.6.1", "browser_specific_settings": { "gecko": { "id": "furbooru-tagging-assistant@thecore.city", diff --git a/package-lock.json b/package-lock.json index 17abb08..49fa2ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "furbooru-tagging-assistant", - "version": "0.6.0.1", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "furbooru-tagging-assistant", - "version": "0.6.0.1", + "version": "0.6.1", "dependencies": { "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-static": "^3.0.10", diff --git a/package.json b/package.json index 23a3fea..e2c0439 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "furbooru-tagging-assistant", - "version": "0.6.0.1", + "version": "0.6.1", "private": true, "type": "module", "scripts": {