2024-06-07 02:44:41 +02:00
|
|
|
/**
|
|
|
|
* Client-side image filtering/spoilering.
|
|
|
|
*/
|
|
|
|
|
2024-06-18 01:36:41 +02:00
|
|
|
import { assertNotUndefined } from './utils/assert';
|
2024-06-07 02:44:41 +02:00
|
|
|
import { $$, escapeHtml } from './utils/dom';
|
|
|
|
import { setupInteractions } from './interactions';
|
|
|
|
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
|
|
|
|
import { TagData, getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag';
|
|
|
|
import { AstMatcher } from './query/types';
|
|
|
|
|
|
|
|
type CallbackType = 'tags' | 'complex';
|
|
|
|
type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void;
|
|
|
|
|
2024-07-04 02:27:59 +02:00
|
|
|
function run(img: HTMLDivElement, tags: TagData[], complex: AstMatcher, runCallback: RunCallback): boolean {
|
2024-06-07 02:44:41 +02:00
|
|
|
const hit = (() => {
|
|
|
|
// Check tags array first to provide more precise filter explanations
|
|
|
|
const hitTags = imageHitsTags(img, tags);
|
|
|
|
if (hitTags.length !== 0) {
|
2024-06-18 01:36:41 +02:00
|
|
|
runCallback(img, hitTags, 'tags');
|
2024-06-07 02:44:41 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No tags matched, try complex filter AST
|
|
|
|
const hitComplex = imageHitsComplex(img, complex);
|
|
|
|
if (hitComplex) {
|
2024-06-18 01:36:41 +02:00
|
|
|
runCallback(img, hitTags, 'complex');
|
2024-06-07 02:44:41 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nothing matched at all, image can be shown
|
|
|
|
return false;
|
|
|
|
})();
|
|
|
|
|
2024-06-18 01:36:41 +02:00
|
|
|
if (hit) {
|
2024-06-07 02:44:41 +02:00
|
|
|
// Disallow negative interaction on image which is not visible
|
2024-06-18 01:36:41 +02:00
|
|
|
window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId));
|
2024-06-07 02:44:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return hit;
|
|
|
|
}
|
|
|
|
|
|
|
|
function bannerImage(tagsHit: TagData[]) {
|
|
|
|
if (tagsHit.length > 0) {
|
|
|
|
return tagsHit[0].spoiler_image_uri || window.booru.hiddenTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
return window.booru.hiddenTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: this approach is not suitable for translations because it depends on
|
|
|
|
// markup embedded in the page adjacent to this text
|
|
|
|
|
|
|
|
function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
2024-07-04 02:27:59 +02:00
|
|
|
const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` : '[HIDDEN] <i>(Complex Filter)</i>';
|
2024-06-07 02:44:41 +02:00
|
|
|
hideThumb(img, bannerImage(tagsHit), bannerText);
|
|
|
|
}
|
|
|
|
|
|
|
|
function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
2024-07-04 02:27:59 +02:00
|
|
|
const bannerText = type === 'tags' ? displayTags(tagsHit) : '<i>(Complex Filter)</i>';
|
2024-06-07 02:44:41 +02:00
|
|
|
spoilerThumb(img, bannerImage(tagsHit), bannerText);
|
|
|
|
}
|
|
|
|
|
|
|
|
function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
2024-07-04 02:27:59 +02:00
|
|
|
const bannerText =
|
|
|
|
type === 'tags'
|
|
|
|
? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
|
|
|
|
: 'This image was hidden by a complex tag expression in ';
|
2024-06-07 02:44:41 +02:00
|
|
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
|
|
|
}
|
|
|
|
|
|
|
|
function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
2024-07-04 02:27:59 +02:00
|
|
|
const bannerText =
|
|
|
|
type === 'tags'
|
|
|
|
? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
|
|
|
|
: 'This image was spoilered by a complex tag expression in ';
|
2024-06-07 02:44:41 +02:00
|
|
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
2024-07-04 02:27:59 +02:00
|
|
|
const hiddenTags = getHiddenTags(),
|
|
|
|
spoileredTags = getSpoileredTags();
|
2024-06-07 02:44:41 +02:00
|
|
|
const { hiddenFilter, spoileredFilter } = window.booru;
|
|
|
|
|
|
|
|
// Image thumb boxes with vote and fave buttons on them
|
|
|
|
$$<HTMLDivElement>('.image-container', node)
|
2024-07-04 02:27:59 +02:00
|
|
|
.filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped))
|
2024-06-07 02:44:41 +02:00
|
|
|
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped))
|
|
|
|
.forEach(img => showThumb(img));
|
|
|
|
|
|
|
|
// Individual image pages and images in posts/comments
|
|
|
|
$$<HTMLDivElement>('.image-show-container', node)
|
2024-07-04 02:27:59 +02:00
|
|
|
.filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped))
|
2024-06-07 02:44:41 +02:00
|
|
|
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped))
|
|
|
|
.forEach(img => showBlock(img));
|
|
|
|
}
|
|
|
|
|
|
|
|
export function initImagesClientside() {
|
|
|
|
window.booru.imagesWithDownvotingDisabled = [];
|
|
|
|
// This fills the imagesWithDownvotingDisabled array
|
|
|
|
filterNode(document);
|
|
|
|
// Once the array is populated, we can initialize interactions
|
|
|
|
setupInteractions();
|
|
|
|
}
|