/** * Client-side image filtering/spoilering. */ import { assertNotUndefined } from './utils/assert'; 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; function run( img: HTMLDivElement, tags: TagData[], complex: AstMatcher, runCallback: RunCallback ): boolean { const hit = (() => { // Check tags array first to provide more precise filter explanations const hitTags = imageHitsTags(img, tags); if (hitTags.length !== 0) { runCallback(img, hitTags, 'tags'); return true; } // No tags matched, try complex filter AST const hitComplex = imageHitsComplex(img, complex); if (hitComplex) { runCallback(img, hitTags, 'complex'); return true; } // Nothing matched at all, image can be shown return false; })(); if (hit) { // Disallow negative interaction on image which is not visible window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId)); } 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 /* eslint-disable indent */ function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` : '[HIDDEN] (Complex Filter)'; hideThumb(img, bannerImage(tagsHit), bannerText); } function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { const bannerText = type === 'tags' ? displayTags(tagsHit) : '(Complex Filter)'; spoilerThumb(img, bannerImage(tagsHit), bannerText); } function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is hidden by ` : 'This image was hidden by a complex tag expression in '; spoilerBlock(img, bannerImage(tagsHit), bannerText); } function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is spoilered by ` : 'This image was spoilered by a complex tag expression in '; spoilerBlock(img, bannerImage(tagsHit), bannerText); } /* eslint-enable indent */ export function filterNode(node: Pick) { const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags(); const { hiddenFilter, spoileredFilter } = window.booru; // Image thumb boxes with vote and fave buttons on them $$('.image-container', node) .filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped)) .filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped)) .forEach(img => showThumb(img)); // Individual image pages and images in posts/comments $$('.image-show-container', node) .filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped)) .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(); }