1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-03-24 23:02:58 +00:00

Merge pull request #160 from koloml/release/0.6.1

Release: 0.6.1
This commit is contained in:
2026-02-26 04:27:47 +04:00
committed by GitHub
10 changed files with 164 additions and 19 deletions

View File

@@ -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,
});

View File

@@ -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",

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.6.0.1",
"version": "0.6.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -34,6 +34,11 @@ export const namespaceCategories: Map<string, string> = new Map([
['series', 'content-fanmade'],
['spoiler', 'spoiler'],
['video', 'content-fanmade'],
...(__CURRENT_SITE__ === 'tantabus' ? <const> [
["prompter", "origin"],
["creator", "origin"],
["generator", "origin"]
] : [])
]);
/**

View File

@@ -1,13 +1,14 @@
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;
#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,25 +51,76 @@ 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 maybeDecodedTagName = decodeTagNameFromLink(linkElement.pathname) ?? '';
linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(maybeDecodedTagName) ?? '';
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[] {
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();
/**
* 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<HTMLElement, string> = new WeakMap();
static findAndInitializeAll() {
for (const container of document.querySelectorAll<HTMLElement>('.block.communication')) {
if (getComponent(container)) {

View File

@@ -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<string, string> = 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) {
@@ -69,7 +102,8 @@ export function decodeTagNameFromLink(tagLink: string): string | null {
return decodeURIComponent(encodedTagName)
.replaceAll(/-[a-z]+-/gi, match => slugEncodedCharacters.get(match) ?? match)
.replaceAll('-', ' ');
.replaceAll('-', ' ')
.replaceAll('+', ' ');
}
/**

View File

@@ -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<TagSettingsFields> {
@@ -18,6 +19,10 @@ export default class TagSettings extends CacheableSettings<TagSettingsFields> {
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<TagSettingsFields> {
async setReplaceLinks(value: boolean) {
return this._writeSetting("replaceLinks", Boolean(value));
}
async setReplaceLinkText(value: boolean) {
return this._writeSetting("replaceLinkText", Boolean(value));
}
}

View File

@@ -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
</CheckboxField>
</FormControl>
{#if $shouldReplaceLinksOnForumPosts}
<FormControl>
<CheckboxField bind:checked={$shouldReplaceTextOfTagLinks}>
Try to replace text on links pointing to tags in forum posts
</CheckboxField>
</FormControl>
{/if}
</FormContainer>

View File

@@ -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));
});
});