mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2026-03-24 23:02:58 +00:00
Merge pull request #150 from koloml/feature/decorated-tag-links-in-forum
Added option to decorate tag links in forum posts
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
65
src/content/components/BlockCommunication.ts
Normal file
65
src/content/components/BlockCommunication.ts
Normal 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
3
src/content/posts.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { BlockCommunication } from "$content/components/BlockCommunication";
|
||||
|
||||
BlockCommunication.findAndInitializeAll();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
9
src/styles/content/posts.scss
Normal file
9
src/styles/content/posts.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@use '$styles/booru-vars';
|
||||
|
||||
.block.communication {
|
||||
.tag {
|
||||
&:hover {
|
||||
color: booru-vars.$resolved-tag-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user