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

Added option to decorate tag links in forum posts

This commit is contained in:
2026-02-23 17:51:27 +04:00
parent 03b0763db4
commit dfdab180ee
9 changed files with 190 additions and 4 deletions

View File

@@ -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<string, string> = 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.

View File

@@ -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<HTMLElement>('.block.communication')) {
if (getComponent(container)) {
continue;
}
new BlockCommunication(container).initialize();
}
}
}

3
src/content/posts.ts Normal file
View File

@@ -0,0 +1,3 @@
import { BlockCommunication } from "$content/components/BlockCommunication";
BlockCommunication.findAndInitializeAll();

View File

@@ -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\/(?<encodedTagName>[^/?#]+)/;
/**
* List of encoded characters from Philomena.
*
* @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/slug.ex#L52-L57
*/
const slugEncodedCharacters: Map<string, string> = 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;
}

View File

@@ -2,6 +2,7 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings";
interface TagSettingsFields {
groupSeparation: boolean;
replaceLinks: boolean;
}
export default class TagSettings extends CacheableSettings<TagSettingsFields> {
@@ -13,7 +14,15 @@ export default class TagSettings extends CacheableSettings<TagSettingsFields> {
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));
}
}

View File

@@ -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
</CheckboxField>
</FormControl>
<FormControl>
<CheckboxField bind:checked={$shouldReplaceLinksOnForumPosts}>
Find and replace links to the tags in the forum posts
</CheckboxField>
</FormControl>
</FormContainer>

View File

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

View File

@@ -0,0 +1,9 @@
@use '$styles/booru-vars';
.block.communication {
.tag {
&:hover {
color: booru-vars.$resolved-tag-color;
}
}
}