mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Merge pull request #276 from philomena-dev/ts-imagesclientside
Convert clientside image filtering scripts to TypeScript
This commit is contained in:
commit
2858f0cabb
4 changed files with 273 additions and 78 deletions
161
assets/js/__tests__/imagesclientside.spec.ts
Normal file
161
assets/js/__tests__/imagesclientside.spec.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { filterNode, initImagesClientside } from '../imagesclientside';
|
||||||
|
import { parseSearch } from '../match_query';
|
||||||
|
import { matchNone } from '../query/boolean';
|
||||||
|
import { assertNotNull } from '../utils/assert';
|
||||||
|
import { $ } from '../utils/dom';
|
||||||
|
|
||||||
|
describe('filterNode', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.booru.hiddenTagList = [];
|
||||||
|
window.booru.spoileredTagList = [];
|
||||||
|
window.booru.ignoredTagList = [];
|
||||||
|
window.booru.imagesWithDownvotingDisabled = [];
|
||||||
|
|
||||||
|
window.booru.hiddenFilter = matchNone();
|
||||||
|
window.booru.spoileredFilter = matchNone();
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeMediaContainer() {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = `
|
||||||
|
<div class="image-container" data-image-id="1" data-image-tags="[1]">
|
||||||
|
<div class="js-spoiler-info-overlay"></div>
|
||||||
|
<picture><img src=""/></picture>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return [ element, assertNotNull($<HTMLDivElement>('.js-spoiler-info-overlay', element)) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should show image media boxes not matching any filter', () => {
|
||||||
|
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(spoilerOverlay).not.toContainHTML('(Complex Filter)');
|
||||||
|
expect(spoilerOverlay).not.toContainHTML('(unknown tag)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should spoiler media boxes spoilered by a tag filter', () => {
|
||||||
|
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||||
|
window.booru.spoileredTagList = [1];
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(spoilerOverlay).toContainHTML('(unknown tag)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should spoiler media boxes spoilered by a complex filter', () => {
|
||||||
|
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||||
|
window.booru.spoileredFilter = parseSearch('id:1');
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(spoilerOverlay).toContainHTML('(Complex Filter)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide media boxes hidden by a tag filter', () => {
|
||||||
|
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||||
|
window.booru.hiddenTagList = [1];
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(spoilerOverlay).toContainHTML('[HIDDEN]');
|
||||||
|
expect(spoilerOverlay).toContainHTML('(unknown tag)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide media boxes hidden by a complex filter', () => {
|
||||||
|
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||||
|
window.booru.hiddenFilter = parseSearch('id:1');
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(spoilerOverlay).toContainHTML('[HIDDEN]');
|
||||||
|
expect(spoilerOverlay).toContainHTML('(Complex Filter)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeImageBlock(): HTMLElement[] {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = `
|
||||||
|
<div class="image-show-container" data-image-id="1" data-image-tags="[1]">
|
||||||
|
<div class="image-filtered hidden">
|
||||||
|
<img src=""/>
|
||||||
|
<span class="filter-explanation"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-show hidden">
|
||||||
|
<picture><img src=""/></picture>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return [
|
||||||
|
element,
|
||||||
|
assertNotNull($<HTMLDivElement>('.image-filtered', element)),
|
||||||
|
assertNotNull($<HTMLDivElement>('.image-show', element)),
|
||||||
|
assertNotNull($<HTMLSpanElement>('.filter-explanation', element))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should show image blocks not matching any filter', () => {
|
||||||
|
const [ container, imageFiltered, imageShow ] = makeImageBlock();
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(imageFiltered).toHaveClass('hidden');
|
||||||
|
expect(imageShow).not.toHaveClass('hidden');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should spoiler image blocks spoilered by a tag filter', () => {
|
||||||
|
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||||
|
window.booru.spoileredTagList = [1];
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(imageFiltered).not.toHaveClass('hidden');
|
||||||
|
expect(imageShow).toHaveClass('hidden');
|
||||||
|
expect(filterExplanation).toContainHTML('spoilered by');
|
||||||
|
expect(filterExplanation).toContainHTML('(unknown tag)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should spoiler image blocks spoilered by a complex filter', () => {
|
||||||
|
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||||
|
window.booru.spoileredFilter = parseSearch('id:1');
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(imageFiltered).not.toHaveClass('hidden');
|
||||||
|
expect(imageShow).toHaveClass('hidden');
|
||||||
|
expect(filterExplanation).toContainHTML('spoilered by');
|
||||||
|
expect(filterExplanation).toContainHTML('complex tag expression');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide image blocks hidden by a tag filter', () => {
|
||||||
|
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||||
|
window.booru.hiddenTagList = [1];
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(imageFiltered).not.toHaveClass('hidden');
|
||||||
|
expect(imageShow).toHaveClass('hidden');
|
||||||
|
expect(filterExplanation).toContainHTML('hidden by');
|
||||||
|
expect(filterExplanation).toContainHTML('(unknown tag)');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide image blocks hidden by a complex filter', () => {
|
||||||
|
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||||
|
window.booru.hiddenFilter = parseSearch('id:1');
|
||||||
|
|
||||||
|
filterNode(container);
|
||||||
|
expect(imageFiltered).not.toHaveClass('hidden');
|
||||||
|
expect(imageShow).toHaveClass('hidden');
|
||||||
|
expect(filterExplanation).toContainHTML('hidden by');
|
||||||
|
expect(filterExplanation).toContainHTML('complex tag expression');
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initImagesClientside', () => {
|
||||||
|
it('should initialize the imagesWithDownvotingDisabled array', () => {
|
||||||
|
initImagesClientside();
|
||||||
|
expect(window.booru.imagesWithDownvotingDisabled).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,76 +0,0 @@
|
||||||
/**
|
|
||||||
* Client-side image filtering/spoilering.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { $$, escapeHtml } from './utils/dom';
|
|
||||||
import { setupInteractions } from './interactions';
|
|
||||||
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
|
|
||||||
import { getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag';
|
|
||||||
|
|
||||||
function runFilter(img, test, runCallback) {
|
|
||||||
if (!test || test.length === 0) return false;
|
|
||||||
|
|
||||||
runCallback(img, test);
|
|
||||||
|
|
||||||
// I don't like this.
|
|
||||||
window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
function filterThumbSimple(img, tagsHit) { hideThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `[HIDDEN] ${displayTags(tagsHit)}`); }
|
|
||||||
function spoilerThumbSimple(img, tagsHit) { spoilerThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, displayTags(tagsHit)); }
|
|
||||||
function filterThumbComplex(img) { hideThumb(img, window.booru.hiddenTag, '[HIDDEN] <i>(Complex Filter)</i>'); }
|
|
||||||
function spoilerThumbComplex(img) { spoilerThumb(img, window.booru.hiddenTag, '<i>(Complex Filter)</i>'); }
|
|
||||||
|
|
||||||
function filterBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `); }
|
|
||||||
function spoilerBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `); }
|
|
||||||
function filterBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was hidden by a complex tag expression in '); }
|
|
||||||
function spoilerBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was spoilered by a complex tag expression in '); }
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
function thumbTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterThumbSimple); }
|
|
||||||
function thumbComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterThumbComplex); }
|
|
||||||
function thumbTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerThumbSimple); }
|
|
||||||
function thumbComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerThumbComplex); }
|
|
||||||
|
|
||||||
function blockTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterBlockSimple); }
|
|
||||||
function blockComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterBlockComplex); }
|
|
||||||
function blockTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerBlockSimple); }
|
|
||||||
function blockComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerBlockComplex); }
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
function filterNode(node = document) {
|
|
||||||
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 => !thumbTagFilter(hiddenTags, img))
|
|
||||||
.filter(img => !thumbComplexFilter(hiddenFilter, img))
|
|
||||||
.filter(img => !thumbTagSpoiler(spoileredTags, img))
|
|
||||||
.filter(img => !thumbComplexSpoiler(spoileredFilter, img))
|
|
||||||
.forEach(img => showThumb(img));
|
|
||||||
|
|
||||||
// Individual image pages and images in posts/comments
|
|
||||||
$$('.image-show-container', node)
|
|
||||||
.filter(img => !blockTagFilter(hiddenTags, img))
|
|
||||||
.filter(img => !blockComplexFilter(hiddenFilter, img))
|
|
||||||
.filter(img => !blockTagSpoiler(spoileredTags, img))
|
|
||||||
.filter(img => !blockComplexSpoiler(spoileredFilter, img))
|
|
||||||
.forEach(img => showBlock(img));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initImagesClientside() {
|
|
||||||
window.booru.imagesWithDownvotingDisabled = [];
|
|
||||||
// This fills the imagesWithDownvotingDisabled array
|
|
||||||
filterNode(document);
|
|
||||||
// Once the array is populated, we can initialize interactions
|
|
||||||
setupInteractions();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { initImagesClientside, filterNode };
|
|
110
assets/js/imagesclientside.ts
Normal file
110
assets/js/imagesclientside.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* 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] <i>(Complex Filter)</i>';
|
||||||
|
hideThumb(img, bannerImage(tagsHit), bannerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
|
const bannerText = type === 'tags' ? displayTags(tagsHit)
|
||||||
|
: '<i>(Complex Filter)</i>';
|
||||||
|
spoilerThumb(img, bannerImage(tagsHit), bannerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
|
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 ';
|
||||||
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
|
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 ';
|
||||||
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-enable indent */
|
||||||
|
|
||||||
|
export function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
||||||
|
const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags();
|
||||||
|
const { hiddenFilter, spoileredFilter } = window.booru;
|
||||||
|
|
||||||
|
// Image thumb boxes with vote and fave buttons on them
|
||||||
|
$$<HTMLDivElement>('.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
|
||||||
|
$$<HTMLDivElement>('.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();
|
||||||
|
}
|
|
@ -28,13 +28,13 @@ function sortTags(hidden: boolean, a: TagData, b: TagData): number {
|
||||||
return a.spoiler_image_uri ? -1 : 1;
|
return a.spoiler_image_uri ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHiddenTags() {
|
export function getHiddenTags(): TagData[] {
|
||||||
return unique(window.booru.hiddenTagList)
|
return unique(window.booru.hiddenTagList)
|
||||||
.map(tagId => getTag(tagId))
|
.map(tagId => getTag(tagId))
|
||||||
.sort(sortTags.bind(null, true));
|
.sort(sortTags.bind(null, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpoileredTags() {
|
export function getSpoileredTags(): TagData[] {
|
||||||
if (window.booru.spoilerType === 'off') return [];
|
if (window.booru.spoilerType === 'off') return [];
|
||||||
|
|
||||||
return unique(window.booru.spoileredTagList)
|
return unique(window.booru.spoileredTagList)
|
||||||
|
|
Loading…
Reference in a new issue