mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2026-02-06 23:32:58 +00:00
Compare commits
9 Commits
0.4.4
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
| fb626a3928 | |||
| 4060e6c44b | |||
| 6098a11115 | |||
| a87d8b94b8 | |||
| c283b96285 | |||
| 02478f0bf0 | |||
| 59c15f27eb | |||
| 134e96bc4c | |||
| 1c05159ddf |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Furbooru Tagging Assistant",
|
||||
"description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "furbooru-tagging-assistant@thecore.city"
|
||||
|
||||
829
package-lock.json
generated
829
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:popup && npm run build:extension",
|
||||
@@ -12,21 +12,20 @@
|
||||
"test:watch": "vitest watch --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.20.3",
|
||||
"@sveltejs/kit": "^2.21.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/chrome": "^0.0.313",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@types/chrome": "^0.0.326",
|
||||
"@types/node": "^22.15.29",
|
||||
"@vitest/coverage-v8": "^3.2.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"sass": "^1.86.3",
|
||||
"svelte": "^5.25.6",
|
||||
"svelte-check": "^4.1.5",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.5",
|
||||
"vitest": "^3.1.1"
|
||||
"jsdom": "^26.1.0",
|
||||
"sass": "^1.89.1",
|
||||
"svelte": "^5.33.14",
|
||||
"svelte-check": "^4.2.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
a {
|
||||
color: colors.$text;
|
||||
|
||||
@@ -3,8 +3,10 @@ import { createMediaBoxTools } from "$lib/components/MediaBoxTools";
|
||||
import { calculateMediaBoxesPositions, initializeMediaBox } from "$lib/components/MediaBoxWrapper";
|
||||
import { createMaintenanceStatusIcon } from "$lib/components/MaintenanceStatusIcon";
|
||||
import { createImageShowFullscreenButton } from "$lib/components/ImageShowFullscreenButton";
|
||||
import { initializeImageListContainer } from "$lib/components/listing/ImageListContainer";
|
||||
|
||||
const mediaBoxes = document.querySelectorAll<HTMLElement>('.media-box');
|
||||
const imageListContainer = document.querySelector<HTMLElement>('#imagelist-container');
|
||||
|
||||
mediaBoxes.forEach(mediaBoxElement => {
|
||||
initializeMediaBox(mediaBoxElement, [
|
||||
@@ -22,3 +24,7 @@ mediaBoxes.forEach(mediaBoxElement => {
|
||||
});
|
||||
|
||||
calculateMediaBoxesPositions(mediaBoxes);
|
||||
|
||||
if (imageListContainer) {
|
||||
initializeImageListContainer(imageListContainer);
|
||||
}
|
||||
|
||||
19
src/lib/components/listing/ImageListContainer.ts
Normal file
19
src/lib/components/listing/ImageListContainer.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { ImageListInfo } from "$lib/components/listing/ImageListInfo";
|
||||
|
||||
export class ImageListContainer extends BaseComponent {
|
||||
#info: ImageListInfo | null = null;
|
||||
|
||||
protected build() {
|
||||
const imageListInfoContainer = this.container.querySelector<HTMLElement>('.js-imagelist-info');
|
||||
|
||||
if (imageListInfoContainer) {
|
||||
this.#info = new ImageListInfo(imageListInfoContainer);
|
||||
this.#info.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeImageListContainer(element: HTMLElement) {
|
||||
new ImageListContainer(element).initialize();
|
||||
}
|
||||
75
src/lib/components/listing/ImageListInfo.ts
Normal file
75
src/lib/components/listing/ImageListInfo.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
|
||||
export class ImageListInfo extends BaseComponent {
|
||||
#tagElement: HTMLElement | null = null;
|
||||
#impliedTags: string[] = [];
|
||||
#showUntaggedImplicationsButton: HTMLAnchorElement = document.createElement('a');
|
||||
|
||||
protected build() {
|
||||
const sectionAfterImage = this.container.querySelector('.tag-info__image + .flex__grow');
|
||||
|
||||
this.#tagElement = sectionAfterImage?.querySelector<HTMLElement>('.tag.dropdown') ?? null;
|
||||
|
||||
const labels = this.container
|
||||
.querySelectorAll<HTMLElement>('.tag-info__image + .flex__grow strong');
|
||||
|
||||
let targetElementToInsertBefore: HTMLElement | null = null;
|
||||
|
||||
for (const potentialListStarter of labels) {
|
||||
if (potentialListStarter.innerText === ImageListInfo.#implicationsStarterText) {
|
||||
targetElementToInsertBefore = potentialListStarter;
|
||||
this.#collectImplicationsFromListStarter(potentialListStarter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#impliedTags.length && targetElementToInsertBefore) {
|
||||
this.#showUntaggedImplicationsButton.href = '#';
|
||||
this.#showUntaggedImplicationsButton.innerText = '(Q)';
|
||||
this.#showUntaggedImplicationsButton.title =
|
||||
'Query untagged implications\n\n' +
|
||||
'This will open the search results with all untagged implications for the current tag.';
|
||||
this.#showUntaggedImplicationsButton.classList.add('detail-link');
|
||||
|
||||
targetElementToInsertBefore.insertAdjacentElement('beforebegin', this.#showUntaggedImplicationsButton);
|
||||
}
|
||||
}
|
||||
|
||||
protected init() {
|
||||
this.#showUntaggedImplicationsButton.addEventListener('click', this.#onShowUntaggedImplicationsClicked.bind(this));
|
||||
}
|
||||
|
||||
#collectImplicationsFromListStarter(listStarter: HTMLElement) {
|
||||
let targetElement: Element | null = listStarter.nextElementSibling;
|
||||
|
||||
while (targetElement) {
|
||||
if (targetElement instanceof HTMLAnchorElement) {
|
||||
this.#impliedTags.push(targetElement.innerText.trim());
|
||||
}
|
||||
|
||||
// First line break is considered the end of the list.
|
||||
if (targetElement instanceof HTMLBRElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
targetElement = targetElement.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
#onShowUntaggedImplicationsClicked(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
url.pathname = '/search';
|
||||
url.search = '';
|
||||
|
||||
const currentTagName = this.#tagElement?.dataset.tagName;
|
||||
|
||||
url.searchParams.set('q', `${currentTagName}, !(${this.#impliedTags.join(", ")})`);
|
||||
|
||||
location.assign(url.href);
|
||||
}
|
||||
|
||||
static #implicationsStarterText = 'Implies:';
|
||||
}
|
||||
33
tests/lib/booru/tag-utils.spec.ts
Normal file
33
tests/lib/booru/tag-utils.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils";
|
||||
|
||||
describe('buildTagsAndAliasesMap', () => {
|
||||
const exampleTag = 'safe';
|
||||
const exampleTagAlias = 'rating:safe';
|
||||
const tagsAndAliases = [exampleTag, exampleTagAlias, 'anthro', 'cat', 'feline', 'mammal', 'male', 'boy'];
|
||||
const tagsOnly = [exampleTag, 'anthro', 'cat', 'feline', 'mammal', 'male'];
|
||||
const mapping = buildTagsAndAliasesMap(tagsAndAliases, tagsOnly);
|
||||
|
||||
it('should return a map of tags', () => {
|
||||
expect(mapping).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should point aliases to their original tags', () => {
|
||||
expect(mapping.get(exampleTagAlias)).toBe(exampleTag);
|
||||
});
|
||||
|
||||
it('should point tags to themselves', () => {
|
||||
expect(mapping.get(exampleTag)).toBe(exampleTag);
|
||||
});
|
||||
|
||||
it('should ignore broken tag aliases and show a warning', () => {
|
||||
vi.spyOn(console, 'warn');
|
||||
|
||||
const brokenMapping = buildTagsAndAliasesMap(
|
||||
['broken alias', 'tag1', 'tag2'],
|
||||
['tag1', 'tag2'],
|
||||
);
|
||||
|
||||
expect(console.warn).toBeCalledTimes(1);
|
||||
expect(brokenMapping.has('broken alias')).toBe(false);
|
||||
});
|
||||
});
|
||||
43
tests/lib/utils.spec.ts
Normal file
43
tests/lib/utils.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { randomString } from "$tests/utils";
|
||||
import { escapeRegExp, findDeepObject } from "$lib/utils";
|
||||
import { randomInt } from "node:crypto";
|
||||
|
||||
describe('findDeepObject', () => {
|
||||
const targetObject = {
|
||||
somewhere: {
|
||||
deep: {
|
||||
stringValue: randomString(),
|
||||
numericValue: randomInt(0, 1000),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('should just return null when nothing is found', () => {
|
||||
const nonExistentValue = findDeepObject(targetObject, ['completely', 'wrong']);
|
||||
expect(nonExistentValue).toBe(null);
|
||||
});
|
||||
|
||||
it('should retrieve something stored deep inside object', () => {
|
||||
const returnedDeepObject = findDeepObject(targetObject, ['somewhere', 'deep']);
|
||||
expect(returnedDeepObject).toBe(targetObject.somewhere.deep);
|
||||
});
|
||||
|
||||
it('should return null if value located on given path is not an object', () => {
|
||||
const returnedForStringValue = findDeepObject(targetObject, ['somewhere', 'deep', 'stringValue']);
|
||||
expect(returnedForStringValue).not.toBe(targetObject.somewhere.deep.stringValue);
|
||||
expect(returnedForStringValue).toBe(null);
|
||||
|
||||
const returnedForNumericValue = findDeepObject(targetObject, ['somewhere', 'deep', 'numericValue']);
|
||||
expect(returnedForNumericValue).not.toBe(targetObject.somewhere.deep.numericValue);
|
||||
expect(returnedForNumericValue).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeRegExp', () => {
|
||||
const specialCharactersToMatch = "$[(?:)]{}()*./\\+?|^";
|
||||
|
||||
it('should sufficiently enough escape special characters', () => {
|
||||
const generatedRegExp = new RegExp(`^${escapeRegExp(specialCharactersToMatch)}$`, 'm');
|
||||
expect(generatedRegExp.test(specialCharactersToMatch)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user