1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-02-07 07:42:59 +00:00

2 Commits

Author SHA1 Message Date
d5dee6c615 Replaced custom-made RegExp escaping utility with @std/regexp 2025-04-08 19:18:27 +04:00
2da37e2316 Installed @std/regexp 1.0.1 from JSR
This is a part of the Deno's bundle of standard libraries, so it should
be somewhat trustworthy. For now, it only includes the escaping function
which I actually need.
2025-04-08 19:16:58 +04:00
12 changed files with 505 additions and 557 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
engine-strict=true
@jsr:registry=https://npm.jsr.io

View File

@@ -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.5",
"version": "0.4.4",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city"

836
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.4.5",
"version": "0.4.4",
"private": true,
"scripts": {
"build": "npm run build:popup && npm run build:extension",
@@ -12,24 +12,26 @@
"test:watch": "vitest watch --coverage"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/kit": "^2.20.3",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/chrome": "^0.0.326",
"@types/node": "^22.15.29",
"@vitest/coverage-v8": "^3.2.0",
"@types/chrome": "^0.0.313",
"@types/node": "^22.14.0",
"@vitest/coverage-v8": "^3.1.1",
"cheerio": "^1.0.0",
"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"
"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"
},
"type": "module",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@std/regexp": "npm:@jsr/std__regexp@^1.0.1",
"lz-string": "^1.5.0"
}
}

View File

@@ -13,7 +13,6 @@
top: 0;
left: 0;
right: 0;
z-index: 10;
a {
color: colors.$text;

View File

@@ -3,10 +3,8 @@ 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, [
@@ -24,7 +22,3 @@ mediaBoxes.forEach(mediaBoxElement => {
});
calculateMediaBoxesPositions(mediaBoxes);
if (imageListContainer) {
initializeImageListContainer(imageListContainer);
}

View File

@@ -1,19 +0,0 @@
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();
}

View File

@@ -1,75 +0,0 @@
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:';
}

View File

@@ -1,6 +1,6 @@
import type { TagDropdownWrapper } from "$lib/components/TagDropdownWrapper";
import TagGroup from "$entities/TagGroup";
import { escapeRegExp } from "$lib/utils";
import { escape as escapeRegExp } from "@std/regexp";
import { emit } from "$lib/components/events/comms";
import { EVENT_TAG_GROUP_RESOLVED } from "$lib/components/events/tag-dropdown-events";

View File

@@ -21,21 +21,3 @@ export function findDeepObject(targetObject: Record<string, any>, path: string[]
return result;
}
/**
* Matches all the characters needing replacement.
*
* Gathered from right here: https://stackoverflow.com/a/3561711/16048617. Because I don't want to introduce some
* library for that.
*/
const unsafeRegExpCharacters: RegExp = /[/\-\\^$*+?.()|[\]{}]/g;
/**
* Escape all the RegExp syntax-related characters in the following value.
* @param value Original value.
* @return Resulting value with all needed characters escaped.
*/
export function escapeRegExp(value: string): string {
unsafeRegExpCharacters.lastIndex = 0;
return value.replace(unsafeRegExpCharacters, "\\$&");
}

View File

@@ -1,33 +0,0 @@
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);
});
});

View File

@@ -1,43 +0,0 @@
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);
});
});