mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
remove clientside support code
This commit is contained in:
parent
5dc4181014
commit
fdda89500b
17 changed files with 9 additions and 1536 deletions
|
@ -1,90 +1,10 @@
|
|||
import { $ } from './utils/dom';
|
||||
import parseSearch from './match_query';
|
||||
import store from './utils/store';
|
||||
|
||||
/* Store a tag locally, marking the retrieval time */
|
||||
function persistTag(tagData) {
|
||||
tagData.fetchedAt = new Date().getTime() / 1000;
|
||||
store.set(`bor_tags_${tagData.id}`, tagData);
|
||||
}
|
||||
|
||||
function isStale(tag) {
|
||||
const now = new Date().getTime() / 1000;
|
||||
return tag.fetchedAt === null || tag.fetchedAt < (now - 604800);
|
||||
}
|
||||
|
||||
function clearTags() {
|
||||
Object.keys(localStorage).forEach(key => {
|
||||
if (key.substring(0, 9) === 'bor_tags_') {
|
||||
store.remove(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Returns a single tag, or a dummy tag object if we don't know about it yet */
|
||||
function getTag(tagId) {
|
||||
const stored = store.get(`bor_tags_${tagId}`);
|
||||
|
||||
if (stored) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return {
|
||||
id: tagId,
|
||||
name: '(unknown tag)',
|
||||
images: 0,
|
||||
spoiler_image_uri: null,
|
||||
};
|
||||
}
|
||||
|
||||
/* Fetches lots of tags in batches and stores them locally */
|
||||
function fetchAndPersistTags(tagIds) {
|
||||
if (!tagIds.length) return;
|
||||
|
||||
const ids = tagIds.slice(0, 40);
|
||||
const remaining = tagIds.slice(41);
|
||||
|
||||
fetch(`/tags/fetch?ids[]=${ids.join('&ids[]=')}`)
|
||||
.then(response => response.json())
|
||||
.then(data => data.tags.forEach(tag => persistTag(tag)))
|
||||
.then(() => fetchAndPersistTags(remaining));
|
||||
}
|
||||
|
||||
/* Figure out which tags in the list we don't know about */
|
||||
function fetchNewOrStaleTags(tagIds) {
|
||||
const fetchIds = [];
|
||||
|
||||
tagIds.forEach(t => {
|
||||
const stored = store.get(`bor_tags_${t}`);
|
||||
if (!stored || isStale(stored)) {
|
||||
fetchIds.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
fetchAndPersistTags(fetchIds);
|
||||
}
|
||||
|
||||
function verifyTagsVersion(latest) {
|
||||
if (store.get('bor_tags_version') !== latest) {
|
||||
clearTags();
|
||||
store.set('bor_tags_version', latest);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFilters() {
|
||||
const tags = window.booru.spoileredTagList
|
||||
.concat(window.booru.hiddenTagList)
|
||||
.filter((a, b, c) => c.indexOf(a) === b);
|
||||
|
||||
verifyTagsVersion(window.booru.tagsVersion);
|
||||
fetchNewOrStaleTags(tags);
|
||||
}
|
||||
|
||||
function unmarshal(data) {
|
||||
try { return JSON.parse(data); } catch (_) { return data; }
|
||||
}
|
||||
|
||||
function loadBooruData() {
|
||||
export function loadBooruData() {
|
||||
const booruData = document.querySelector('.js-datastore').dataset;
|
||||
|
||||
// Assign all elements to booru because lazy
|
||||
|
@ -92,22 +12,8 @@ function loadBooruData() {
|
|||
window.booru[prop] = unmarshal(booruData[prop]);
|
||||
}
|
||||
|
||||
window.booru.hiddenFilter = parseSearch(window.booru.hiddenFilter);
|
||||
window.booru.spoileredFilter = parseSearch(window.booru.spoileredFilter);
|
||||
|
||||
// Fetch tag metadata and set up filtering
|
||||
initializeFilters();
|
||||
|
||||
// CSRF
|
||||
window.booru.csrfToken = $('meta[name="csrf-token"]').content;
|
||||
}
|
||||
|
||||
function BooruOnRails() {
|
||||
this.apiEndpoint = '/api/v2/';
|
||||
this.hiddenTag = '/images/tagblocked.svg';
|
||||
this.tagsVersion = 5;
|
||||
}
|
||||
|
||||
window.booru = new BooruOnRails();
|
||||
|
||||
export { getTag, loadBooruData };
|
||||
window.booru = {};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { $, $$ } from './utils/dom';
|
||||
import { fetchHtml, handleError } from './utils/requests';
|
||||
import { showBlock } from './utils/image';
|
||||
import { addTag } from './tagsinput';
|
||||
|
||||
// Event types and any qualifying conditions - return true to not run action
|
||||
|
@ -72,10 +71,7 @@ const actions = {
|
|||
.catch(() => newTab.textContent = 'Error!');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
unfilter(data) { showBlock(data.el.closest('.image-show-container')); },
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Use this function to apply a callback to elements matching the selectors
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import { $ } from './utils/dom';
|
||||
import { showOwnedComments } from './communications/comment';
|
||||
import { filterNode } from './imagesclientside';
|
||||
import { fetchHtml } from './utils/requests';
|
||||
|
||||
function handleError(response) {
|
||||
|
@ -94,9 +93,6 @@ function insertParentPost(data, clickedLink, fullComment) {
|
|||
// Add class active_reply_link to the clicked link
|
||||
clickedLink.classList.add('active_reply_link');
|
||||
|
||||
// Filter images (if any) in the loaded comment
|
||||
filterNode(fullComment.previousSibling);
|
||||
|
||||
}
|
||||
|
||||
function clearParentPost(clickedLink, fullComment) {
|
||||
|
@ -125,9 +121,6 @@ function displayComments(container, commentsHtml) {
|
|||
// Execute timeago on comments
|
||||
window.booru.timeAgo(document.getElementsByTagName('time'));
|
||||
|
||||
// Filter images in the comments
|
||||
filterNode(container);
|
||||
|
||||
// Show options on own comments
|
||||
showOwnedComments();
|
||||
|
||||
|
@ -171,7 +164,6 @@ function setupComments() {
|
|||
loadComments(false);
|
||||
}
|
||||
else {
|
||||
filterNode(comments);
|
||||
showOwnedComments();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
|
@ -1,871 +0,0 @@
|
|||
/**
|
||||
* booru.match_query: A port and modification of the search_parser library for
|
||||
* performing client-side filtering.
|
||||
*/
|
||||
|
||||
const tokenList = [
|
||||
['fuzz', /^~(?:\d+(\.\d+)?|\.\d+)/],
|
||||
['boost', /^\^[\-\+]?\d+(\.\d+)?/],
|
||||
['quoted_lit', /^\s*"(?:(?:[^"]|\\")+)"/],
|
||||
['lparen', /^\s*\(\s*/],
|
||||
['rparen', /^\s*\)\s*/],
|
||||
['and_op', /^\s*(?:\&\&|AND)\s+/],
|
||||
['and_op', /^\s*,\s*/],
|
||||
['or_op', /^\s*(?:\|\||OR)\s+/],
|
||||
['not_op', /^\s*NOT(?:\s+|(?=\())/],
|
||||
['not_op', /^\s*[\!\-]\s*/],
|
||||
['space', /^\s+/],
|
||||
['word', /^(?:[^\s,\(\)\^~]|\\[\s,\(\)\^~])+/],
|
||||
['word', /^(?:[^\s,\(\)]|\\[\s,\(\)])+/]
|
||||
],
|
||||
numberFields = ['id', 'width', 'height', 'aspect_ratio',
|
||||
'comment_count', 'score', 'upvotes', 'downvotes',
|
||||
'faves'],
|
||||
dateFields = ['created_at'],
|
||||
literalFields = ['tags', 'orig_sha512_hash', 'sha512_hash',
|
||||
'score', 'uploader', 'source_url', 'description'],
|
||||
termSpaceToImageField = {
|
||||
tags: 'data-image-tag-aliases',
|
||||
score: 'data-score',
|
||||
upvotes: 'data-upvotes',
|
||||
downvotes: 'data-downvotes',
|
||||
uploader: 'data-uploader',
|
||||
// Yeah, I don't think this is reasonably supportable.
|
||||
// faved_by: 'data-faved-by',
|
||||
id: 'data-image-id',
|
||||
width: 'data-width',
|
||||
height: 'data-height',
|
||||
aspect_ratio: 'data-aspect-ratio',
|
||||
comment_count: 'data-comment-count',
|
||||
source_url: 'data-source-url',
|
||||
faves: 'data-faves',
|
||||
sha512_hash: 'data-sha512',
|
||||
orig_sha512_hash: 'data-orig-sha512',
|
||||
created_at: 'data-created-at'
|
||||
};
|
||||
|
||||
|
||||
function SearchTerm(termStr, options) {
|
||||
this.term = termStr.trim();
|
||||
this.parsed = false;
|
||||
}
|
||||
|
||||
SearchTerm.prototype.append = function(substr) {
|
||||
this.term += substr;
|
||||
this.parsed = false;
|
||||
};
|
||||
|
||||
SearchTerm.prototype.parseRangeField = function(field) {
|
||||
let qual;
|
||||
|
||||
if (numberFields.indexOf(field) !== -1) {
|
||||
return [field, 'eq', 'number'];
|
||||
}
|
||||
|
||||
if (dateFields.indexOf(field) !== -1) {
|
||||
return [field, 'eq', 'date'];
|
||||
}
|
||||
|
||||
qual = /^(\w+)\.([lg]te?|eq)$/.exec(field);
|
||||
|
||||
if (qual) {
|
||||
if (numberFields.indexOf(qual[1]) !== -1) {
|
||||
return [qual[1], qual[2], 'number'];
|
||||
}
|
||||
|
||||
if (dateFields.indexOf(qual[1]) !== -1) {
|
||||
return [qual[1], qual[2], 'date'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
SearchTerm.prototype.parseRelativeDate = function(dateVal, qual) {
|
||||
const match = /(\d+) (second|minute|hour|day|week|month|year)s? ago/.exec(dateVal);
|
||||
const bounds = {
|
||||
second: 1000,
|
||||
minute: 60000,
|
||||
hour: 3600000,
|
||||
day: 86400000,
|
||||
week: 604800000,
|
||||
month: 2592000000,
|
||||
year: 31536000000
|
||||
};
|
||||
|
||||
if (match) {
|
||||
const amount = parseInt(match[1], 10);
|
||||
const scale = bounds[match[2]];
|
||||
|
||||
const now = new Date().getTime();
|
||||
const bottomDate = new Date(now - (amount * scale));
|
||||
const topDate = new Date(now - ((amount - 1) * scale));
|
||||
|
||||
switch (qual) {
|
||||
case 'lte':
|
||||
return [bottomDate, 'lt'];
|
||||
case 'gte':
|
||||
return [bottomDate, 'gte'];
|
||||
case 'lt':
|
||||
return [bottomDate, 'lt'];
|
||||
case 'gt':
|
||||
return [bottomDate, 'gte'];
|
||||
default:
|
||||
return [[bottomDate, topDate], 'eq'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw `Cannot parse date string: ${dateVal}`;
|
||||
}
|
||||
};
|
||||
|
||||
SearchTerm.prototype.parseAbsoluteDate = function(dateVal, qual) {
|
||||
let parseRes = [
|
||||
/^(\d{4})/,
|
||||
/^\-(\d{2})/,
|
||||
/^\-(\d{2})/,
|
||||
/^(?:\s+|T|t)(\d{2})/,
|
||||
/^:(\d{2})/,
|
||||
/^:(\d{2})/
|
||||
],
|
||||
timeZoneOffset = [0, 0],
|
||||
timeData = [0, 0, 1, 0, 0, 0],
|
||||
bottomDate = null,
|
||||
topDate = null,
|
||||
i,
|
||||
match,
|
||||
origDateVal = dateVal;
|
||||
|
||||
match = /([\+\-])(\d{2}):(\d{2})$/.exec(dateVal);
|
||||
if (match) {
|
||||
timeZoneOffset[0] = parseInt(match[2], 10);
|
||||
timeZoneOffset[1] = parseInt(match[3], 10);
|
||||
if (match[1] === '-') {
|
||||
timeZoneOffset[0] *= -1;
|
||||
timeZoneOffset[1] *= -1;
|
||||
}
|
||||
dateVal = dateVal.substr(0, dateVal.length - 6);
|
||||
}
|
||||
else {
|
||||
dateVal = dateVal.replace(/[Zz]$/, '');
|
||||
}
|
||||
|
||||
for (i = 0; i < parseRes.length; i += 1) {
|
||||
if (dateVal.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
match = parseRes[i].exec(dateVal);
|
||||
if (match) {
|
||||
if (i === 1) {
|
||||
timeData[i] = parseInt(match[1], 10) - 1;
|
||||
}
|
||||
else {
|
||||
timeData[i] = parseInt(match[1], 10);
|
||||
}
|
||||
dateVal = dateVal.substr(
|
||||
match[0].length, dateVal.length - match[0].length
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw `Cannot parse date string: ${origDateVal}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (dateVal.length > 0) {
|
||||
throw `Cannot parse date string: ${origDateVal}`;
|
||||
}
|
||||
|
||||
// Apply the user-specified time zone offset. The JS Date constructor
|
||||
// is very flexible here.
|
||||
timeData[3] -= timeZoneOffset[0];
|
||||
timeData[4] -= timeZoneOffset[1];
|
||||
|
||||
switch (qual) {
|
||||
case 'lte':
|
||||
timeData[i - 1] += 1;
|
||||
return [Date.UTC.apply(Date, timeData), 'lt'];
|
||||
case 'gte':
|
||||
return [Date.UTC.apply(Date, timeData), 'gte'];
|
||||
case 'lt':
|
||||
return [Date.UTC.apply(Date, timeData), 'lt'];
|
||||
case 'gt':
|
||||
timeData[i - 1] += 1;
|
||||
return [Date.UTC.apply(Date, timeData), 'gte'];
|
||||
default:
|
||||
bottomDate = Date.UTC.apply(Date, timeData);
|
||||
timeData[i - 1] += 1;
|
||||
topDate = Date.UTC.apply(Date, timeData);
|
||||
return [[bottomDate, topDate], 'eq'];
|
||||
}
|
||||
};
|
||||
|
||||
SearchTerm.prototype.parseDate = function(dateVal, qual) {
|
||||
try {
|
||||
return this.parseAbsoluteDate(dateVal, qual);
|
||||
}
|
||||
catch (_) {
|
||||
return this.parseRelativeDate(dateVal, qual);
|
||||
}
|
||||
};
|
||||
|
||||
SearchTerm.prototype.parse = function(substr) {
|
||||
let matchArr,
|
||||
rangeParsing,
|
||||
candidateTermSpace,
|
||||
termCandidate;
|
||||
|
||||
this.wildcardable = !this.fuzz && !/^"([^"]|\\")+"$/.test(this.term);
|
||||
|
||||
if (!this.wildcardable && !this.fuzz) {
|
||||
this.term = this.term.substr(1, this.term.length - 2);
|
||||
}
|
||||
|
||||
this.term = this._normalizeTerm();
|
||||
|
||||
// N.B.: For the purposes of this parser, boosting effects are ignored.
|
||||
|
||||
// Default.
|
||||
this.termSpace = 'tags';
|
||||
this.termType = 'literal';
|
||||
|
||||
matchArr = this.term.split(':');
|
||||
|
||||
if (matchArr.length > 1) {
|
||||
candidateTermSpace = matchArr[0];
|
||||
termCandidate = matchArr.slice(1).join(':');
|
||||
rangeParsing = this.parseRangeField(candidateTermSpace);
|
||||
|
||||
if (rangeParsing) {
|
||||
this.termSpace = rangeParsing[0];
|
||||
this.termType = rangeParsing[2];
|
||||
|
||||
if (this.termType === 'date') {
|
||||
rangeParsing = this.parseDate(termCandidate, rangeParsing[1]);
|
||||
this.term = rangeParsing[0];
|
||||
this.compare = rangeParsing[1];
|
||||
}
|
||||
else {
|
||||
this.term = parseFloat(termCandidate);
|
||||
this.compare = rangeParsing[1];
|
||||
}
|
||||
|
||||
this.wildcardable = false;
|
||||
}
|
||||
else if (literalFields.indexOf(candidateTermSpace) !== -1) {
|
||||
this.termType = 'literal';
|
||||
this.term = termCandidate;
|
||||
this.termSpace = candidateTermSpace;
|
||||
}
|
||||
else if (candidateTermSpace == 'my') {
|
||||
this.termType = 'my';
|
||||
this.termSpace = termCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.wildcardable) {
|
||||
// Transforms wildcard match into regular expression.
|
||||
// A custom NFA with caching may be more sophisticated but not
|
||||
// likely to be faster.
|
||||
this.term = new RegExp(
|
||||
`^${
|
||||
this.term.replace(/([.+^$[\]\\(){}|-])/g, '\\$1')
|
||||
.replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*')
|
||||
.replace(/^(?:\\\\)*\*/g, '.*')
|
||||
.replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?')
|
||||
.replace(/^(?:\\\\)*\?/g, '.?')
|
||||
}$`, 'i'
|
||||
);
|
||||
}
|
||||
|
||||
// Update parse status flag to indicate the new properties are ready.
|
||||
this.parsed = true;
|
||||
};
|
||||
|
||||
SearchTerm.prototype._normalizeTerm = function() {
|
||||
if (!this.wildcardable) {
|
||||
return this.term.replace('\"', '"');
|
||||
}
|
||||
return this.term.replace(/\\([^\*\?])/g, '$1');
|
||||
};
|
||||
|
||||
SearchTerm.prototype.fuzzyMatch = function(targetStr) {
|
||||
let targetDistance,
|
||||
i,
|
||||
j,
|
||||
// Work vectors, representing the last three populated
|
||||
// rows of the dynamic programming matrix of the iterative
|
||||
// optimal string alignment calculation.
|
||||
v0 = [],
|
||||
v1 = [],
|
||||
v2 = [],
|
||||
temp;
|
||||
|
||||
if (this.fuzz < 1.0) {
|
||||
targetDistance = targetStr.length * (1.0 - this.fuzz);
|
||||
}
|
||||
else {
|
||||
targetDistance = this.fuzz;
|
||||
}
|
||||
|
||||
targetStr = targetStr.toLowerCase();
|
||||
|
||||
for (i = 0; i <= targetStr.length; i += 1) {
|
||||
v1.push(i);
|
||||
}
|
||||
|
||||
for (i = 0; i < this.term.length; i += 1) {
|
||||
v2[0] = i;
|
||||
for (j = 0; j < targetStr.length; j += 1) {
|
||||
const cost = this.term[i] === targetStr[j] ? 0 : 1;
|
||||
v2[j + 1] = Math.min(
|
||||
// Deletion.
|
||||
v1[j + 1] + 1,
|
||||
// Insertion.
|
||||
v2[j] + 1,
|
||||
// Substitution or No Change.
|
||||
v1[j] + cost
|
||||
);
|
||||
if (i > 1 && j > 1 && this.term[i] === targetStr[j - 1] &&
|
||||
targetStr[i - 1] === targetStr[j]) {
|
||||
v2[j + 1] = Math.min(v2[j], v0[j - 1] + cost);
|
||||
}
|
||||
}
|
||||
// Rotate dem vec pointers bra.
|
||||
temp = v0;
|
||||
v0 = v1;
|
||||
v1 = v2;
|
||||
v2 = temp;
|
||||
}
|
||||
|
||||
return v1[targetStr.length] <= targetDistance;
|
||||
};
|
||||
|
||||
SearchTerm.prototype.exactMatch = function(targetStr) {
|
||||
return this.term.toLowerCase() === targetStr.toLowerCase();
|
||||
};
|
||||
|
||||
SearchTerm.prototype.wildcardMatch = function(targetStr) {
|
||||
return this.term.test(targetStr);
|
||||
};
|
||||
|
||||
SearchTerm.prototype.interactionMatch = function(imageID, type, interaction, interactions) {
|
||||
let ret = false;
|
||||
|
||||
interactions.forEach(v => {
|
||||
if (v.image_id == imageID && v.interaction_type == type && (interaction == null || v.value == interaction)) {
|
||||
ret = true;
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
SearchTerm.prototype.match = function(target) {
|
||||
let ret = false,
|
||||
ohffs = this,
|
||||
compFunc,
|
||||
numbuh,
|
||||
date;
|
||||
|
||||
if (!this.parsed) {
|
||||
this.parse();
|
||||
}
|
||||
|
||||
if (this.termType === 'literal') {
|
||||
// Literal matching.
|
||||
if (this.fuzz) {
|
||||
compFunc = this.fuzzyMatch;
|
||||
}
|
||||
else if (this.wildcardable) {
|
||||
compFunc = this.wildcardMatch;
|
||||
}
|
||||
else {
|
||||
compFunc = this.exactMatch;
|
||||
}
|
||||
|
||||
if (this.termSpace === 'tags') {
|
||||
target.getAttribute('data-image-tag-aliases').split(', ').every(
|
||||
str => {
|
||||
if (compFunc.call(ohffs, str)) {
|
||||
ret = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
ret = compFunc.call(
|
||||
this, target.getAttribute(termSpaceToImageField[this.termSpace])
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (this.termType === 'my' && window.booru.interactions.length > 0) {
|
||||
// Should work with most my:conditions except watched.
|
||||
switch (this.termSpace) {
|
||||
case 'faves':
|
||||
ret = this.interactionMatch(target.getAttribute('data-image-id'), 'faved', null, window.booru.interactions);
|
||||
|
||||
break;
|
||||
case 'upvotes':
|
||||
ret = this.interactionMatch(target.getAttribute('data-image-id'), 'voted', 'up', window.booru.interactions);
|
||||
|
||||
break;
|
||||
case 'downvotes':
|
||||
ret = this.interactionMatch(target.getAttribute('data-image-id'), 'voted', 'down', window.booru.interactions);
|
||||
|
||||
break;
|
||||
default:
|
||||
ret = false; // Other my: interactions aren't supported, return false to prevent them from triggering spoiler.
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (this.termType === 'date') {
|
||||
// Date matching.
|
||||
date = (new Date(
|
||||
target.getAttribute(termSpaceToImageField[this.termSpace])
|
||||
)).getTime();
|
||||
|
||||
switch (this.compare) {
|
||||
// The open-left, closed-right date range specified by the
|
||||
// date/time format limits the types of comparisons that are
|
||||
// done compared to numeric ranges.
|
||||
case 'lt':
|
||||
ret = this.term > date;
|
||||
break;
|
||||
case 'gte':
|
||||
ret = this.term <= date;
|
||||
break;
|
||||
default:
|
||||
ret = this.term[0] <= date && this.term[1] > date;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Range matching.
|
||||
numbuh = parseFloat(
|
||||
target.getAttribute(termSpaceToImageField[this.termSpace])
|
||||
);
|
||||
|
||||
if (isNaN(this.term)) {
|
||||
ret = false;
|
||||
}
|
||||
else if (this.fuzz) {
|
||||
ret = this.term <= numbuh + this.fuzz &&
|
||||
this.term + this.fuzz >= numbuh;
|
||||
}
|
||||
else {
|
||||
switch (this.compare) {
|
||||
case 'lt':
|
||||
ret = this.term > numbuh;
|
||||
break;
|
||||
case 'gt':
|
||||
ret = this.term < numbuh;
|
||||
break;
|
||||
case 'lte':
|
||||
ret = this.term >= numbuh;
|
||||
break;
|
||||
case 'gte':
|
||||
ret = this.term <= numbuh;
|
||||
break;
|
||||
default:
|
||||
ret = this.term === numbuh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
function generateLexArray(searchStr, options) {
|
||||
let opQueue = [],
|
||||
searchTerm = null,
|
||||
boost = null,
|
||||
fuzz = null,
|
||||
lparenCtr = 0,
|
||||
negate = false,
|
||||
groupNegate = false,
|
||||
tokenStack = [],
|
||||
boostFuzzStr = '';
|
||||
|
||||
while (searchStr.length > 0) {
|
||||
tokenList.every(tokenArr => {
|
||||
let tokenName = tokenArr[0],
|
||||
tokenRE = tokenArr[1],
|
||||
match = tokenRE.exec(searchStr),
|
||||
balanced, op;
|
||||
|
||||
if (match) {
|
||||
match = match[0];
|
||||
|
||||
if (Boolean(searchTerm) && (
|
||||
['and_op', 'or_op'].indexOf(tokenName) !== -1 ||
|
||||
tokenName === 'rparen' && lparenCtr === 0)) {
|
||||
// Set options.
|
||||
searchTerm.boost = boost;
|
||||
searchTerm.fuzz = fuzz;
|
||||
// Push to stack.
|
||||
tokenStack.push(searchTerm);
|
||||
// Reset term and options data.
|
||||
searchTerm = fuzz = boost = null;
|
||||
boostFuzzStr = '';
|
||||
lparenCtr = 0;
|
||||
|
||||
if (negate) {
|
||||
tokenStack.push('not_op');
|
||||
negate = false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (tokenName) {
|
||||
case 'and_op':
|
||||
while (opQueue[0] === 'and_op') {
|
||||
tokenStack.push(opQueue.shift());
|
||||
}
|
||||
opQueue.unshift('and_op');
|
||||
break;
|
||||
case 'or_op':
|
||||
while (opQueue[0] === 'and_op' || opQueue[0] === 'or_op') {
|
||||
tokenStack.push(opQueue.shift());
|
||||
}
|
||||
opQueue.unshift('or_op');
|
||||
break;
|
||||
case 'not_op':
|
||||
if (searchTerm) {
|
||||
// We're already inside a search term, so it does
|
||||
// not apply, obv.
|
||||
searchTerm.append(match);
|
||||
}
|
||||
else {
|
||||
negate = !negate;
|
||||
}
|
||||
break;
|
||||
case 'lparen':
|
||||
if (searchTerm) {
|
||||
// If we are inside the search term, do not error
|
||||
// out just yet; instead, consider it as part of
|
||||
// the search term, as a user convenience.
|
||||
searchTerm.append(match);
|
||||
lparenCtr += 1;
|
||||
}
|
||||
else {
|
||||
opQueue.unshift('lparen');
|
||||
groupNegate = negate;
|
||||
negate = false;
|
||||
}
|
||||
break;
|
||||
case 'rparen':
|
||||
if (lparenCtr > 0) {
|
||||
searchTerm.append(match);
|
||||
lparenCtr -= 1;
|
||||
}
|
||||
else {
|
||||
balanced = false;
|
||||
while (opQueue.length) {
|
||||
op = opQueue.shift();
|
||||
if (op === 'lparen') {
|
||||
balanced = true;
|
||||
break;
|
||||
}
|
||||
tokenStack.push(op);
|
||||
}
|
||||
}
|
||||
if (groupNegate) {
|
||||
tokenStack.push('not_op');
|
||||
groupNegate = false;
|
||||
}
|
||||
break;
|
||||
case 'fuzz':
|
||||
if (searchTerm) {
|
||||
// For this and boost operations, we store the
|
||||
// current match so far to a temporary string in
|
||||
// case this is actually inside the term.
|
||||
fuzz = parseFloat(match.substr(1));
|
||||
boostFuzzStr += match;
|
||||
}
|
||||
else {
|
||||
searchTerm = new SearchTerm(match, options);
|
||||
}
|
||||
break;
|
||||
case 'boost':
|
||||
if (searchTerm) {
|
||||
boost = match.substr(1);
|
||||
boostFuzzStr += match;
|
||||
}
|
||||
else {
|
||||
searchTerm = new SearchTerm(match, options);
|
||||
}
|
||||
break;
|
||||
case 'quoted_lit':
|
||||
if (searchTerm) {
|
||||
searchTerm.append(match);
|
||||
}
|
||||
else {
|
||||
searchTerm = new SearchTerm(match, options);
|
||||
}
|
||||
break;
|
||||
case 'word':
|
||||
if (searchTerm) {
|
||||
if (fuzz || boost) {
|
||||
boost = fuzz = null;
|
||||
searchTerm.append(boostFuzzStr);
|
||||
boostFuzzStr = '';
|
||||
}
|
||||
searchTerm.append(match);
|
||||
}
|
||||
else {
|
||||
searchTerm = new SearchTerm(match, options);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Append extra spaces within search terms.
|
||||
if (searchTerm) {
|
||||
searchTerm.append(match);
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate string and restart the token tests.
|
||||
searchStr = searchStr.substr(
|
||||
match.length, searchStr.length - match.length
|
||||
);
|
||||
|
||||
// Break since we have found a match.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Append final tokens to the stack, starting with the search term.
|
||||
if (searchTerm) {
|
||||
searchTerm.boost = boost;
|
||||
searchTerm.fuzz = fuzz;
|
||||
tokenStack.push(searchTerm);
|
||||
}
|
||||
if (negate) {
|
||||
tokenStack.push('not_op');
|
||||
}
|
||||
|
||||
if (opQueue.indexOf('rparen') !== -1 ||
|
||||
opQueue.indexOf('lparen') !== -1) {
|
||||
throw 'Mismatched parentheses.';
|
||||
}
|
||||
|
||||
// Memory-efficient concatenation of remaining operators queue to the
|
||||
// token stack.
|
||||
tokenStack.push.apply(tokenStack, opQueue);
|
||||
|
||||
return tokenStack;
|
||||
}
|
||||
|
||||
function parseTokens(lexicalArray) {
|
||||
let operandStack = [],
|
||||
negate, op1, op2, parsed;
|
||||
lexicalArray.forEach((token, i) => {
|
||||
if (token !== 'not_op') {
|
||||
negate = lexicalArray[i + 1] === 'not_op';
|
||||
|
||||
if (typeof token === 'string') {
|
||||
op2 = operandStack.pop();
|
||||
op1 = operandStack.pop();
|
||||
|
||||
if (typeof op1 === 'undefined' || typeof op2 === 'undefined') {
|
||||
throw 'Missing operand.';
|
||||
}
|
||||
|
||||
operandStack.push(new SearchAST(token, negate, op1, op2));
|
||||
}
|
||||
else {
|
||||
if (negate) {
|
||||
operandStack.push(new SearchAST(null, true, token));
|
||||
}
|
||||
else {
|
||||
operandStack.push(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (operandStack.length > 1) {
|
||||
throw 'Missing operator.';
|
||||
}
|
||||
|
||||
op1 = operandStack.pop();
|
||||
|
||||
if (typeof op1 === 'undefined') {
|
||||
return new SearchAST();
|
||||
}
|
||||
|
||||
if (isTerminal(op1)) {
|
||||
return new SearchAST(null, false, op1);
|
||||
}
|
||||
|
||||
return op1;
|
||||
}
|
||||
|
||||
function parseSearch(searchStr, options) {
|
||||
return parseTokens(generateLexArray(searchStr, options));
|
||||
}
|
||||
|
||||
function isTerminal(operand) {
|
||||
// Whether operand is a terminal SearchTerm.
|
||||
return typeof operand.term !== 'undefined';
|
||||
}
|
||||
|
||||
function SearchAST(op, negate, leftOperand, rightOperand) {
|
||||
this.negate = Boolean(negate);
|
||||
this.leftOperand = leftOperand || null;
|
||||
this.op = op || null;
|
||||
this.rightOperand = rightOperand || null;
|
||||
}
|
||||
|
||||
function combineOperands(ast1, ast2, parentAST) {
|
||||
if (parentAST.op === 'and_op') {
|
||||
ast1 = ast1 && ast2;
|
||||
}
|
||||
else {
|
||||
ast1 = ast1 || ast2;
|
||||
}
|
||||
|
||||
if (parentAST.negate) {
|
||||
return !ast1;
|
||||
}
|
||||
|
||||
return ast1;
|
||||
}
|
||||
|
||||
// Evaluation of the AST in regard to a target image
|
||||
SearchAST.prototype.hitsImage = function(image) {
|
||||
let treeStack = [],
|
||||
// Left side node.
|
||||
ast1 = this,
|
||||
// Right side node.
|
||||
ast2,
|
||||
// Parent node of the current subtree.
|
||||
parentAST;
|
||||
|
||||
// Build the initial tree node traversal stack, of the "far left" side.
|
||||
// The general idea is to accumulate from the bottom and make stacks
|
||||
// of right-hand subtrees that themselves accumulate upward. The left
|
||||
// side node, ast1, will always be a Boolean representing the left-side
|
||||
// evaluated value, up to the current subtree (parentAST).
|
||||
while (!isTerminal(ast1)) {
|
||||
treeStack.push(ast1);
|
||||
ast1 = ast1.leftOperand;
|
||||
|
||||
if (!ast1) {
|
||||
// Empty tree.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ast1 = ast1.match(image);
|
||||
treeStack.push(ast1);
|
||||
|
||||
while (treeStack.length > 0) {
|
||||
parentAST = treeStack.pop();
|
||||
|
||||
if (parentAST === null) {
|
||||
// We are at the end of a virtual stack for a right node
|
||||
// subtree. We switch the result of this stack from left
|
||||
// (ast1) to right (ast2), pop the original left node,
|
||||
// and finally pop the parent subtree itself. See near the
|
||||
// end of this function to view how this is populated.
|
||||
ast2 = ast1;
|
||||
ast1 = treeStack.pop();
|
||||
parentAST = treeStack.pop();
|
||||
}
|
||||
else {
|
||||
// First, check to see if we can do a short-circuit
|
||||
// evaluation to skip evaluating the right side entirely.
|
||||
if (!ast1 && parentAST.op === 'and_op') {
|
||||
ast1 = parentAST.negate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ast1 && parentAST.op === 'or_op') {
|
||||
ast1 = !parentAST.negate;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are not at the end of a stack, grab the right
|
||||
// node. The left node (ast1) is currently a terminal Boolean.
|
||||
ast2 = parentAST.rightOperand;
|
||||
}
|
||||
|
||||
if (typeof ast2 === 'boolean') {
|
||||
ast1 = combineOperands(ast1, ast2, parentAST);
|
||||
}
|
||||
else if (!ast2) {
|
||||
// A subtree with a single node. This is generally the case
|
||||
// for negated tokens.
|
||||
if (parentAST.negate) {
|
||||
ast1 = !ast1;
|
||||
}
|
||||
}
|
||||
else if (isTerminal(ast2)) {
|
||||
// We are finally at a leaf and can evaluate.
|
||||
ast2 = ast2.match(image);
|
||||
ast1 = combineOperands(ast1, ast2, parentAST);
|
||||
}
|
||||
else {
|
||||
// We are at a node whose right side is a new subtree.
|
||||
// We will build a new "virtual" stack, but instead of
|
||||
// building a new Array, we can insert a null object as a
|
||||
// marker.
|
||||
treeStack.push(parentAST, ast1, null);
|
||||
|
||||
do {
|
||||
treeStack.push(ast2);
|
||||
ast2 = ast2.leftOperand;
|
||||
} while (!isTerminal(ast2));
|
||||
|
||||
ast1 = ast2.match(image);
|
||||
}
|
||||
}
|
||||
|
||||
return ast1;
|
||||
};
|
||||
|
||||
SearchAST.prototype.dumpTree = function() {
|
||||
// Dumps to string a simple diagram of the syntax tree structure
|
||||
// (starting with this object as the root) for debugging purposes.
|
||||
let retStrArr = [],
|
||||
treeQueue = [['', this]],
|
||||
treeArr,
|
||||
prefix,
|
||||
tree;
|
||||
|
||||
while (treeQueue.length > 0) {
|
||||
treeArr = treeQueue.shift();
|
||||
prefix = treeArr[0];
|
||||
tree = treeArr[1];
|
||||
|
||||
if (isTerminal(tree)) {
|
||||
retStrArr.push(`${prefix}-> ${tree.term}`);
|
||||
}
|
||||
else {
|
||||
if (tree.negate) {
|
||||
retStrArr.push(`${prefix}+ NOT_OP`);
|
||||
prefix += '\t';
|
||||
}
|
||||
if (tree.op) {
|
||||
retStrArr.push(`${prefix}+ ${tree.op.toUpperCase()}`);
|
||||
prefix += '\t';
|
||||
treeQueue.unshift([prefix, tree.rightOperand]);
|
||||
treeQueue.unshift([prefix, tree.leftOperand]);
|
||||
}
|
||||
else {
|
||||
treeQueue.unshift([prefix, tree.leftOperand]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retStrArr.join('\n');
|
||||
};
|
||||
|
||||
export default parseSearch;
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
|
||||
import { fetchJson } from './utils/requests';
|
||||
import { filterNode } from './imagesclientside';
|
||||
|
||||
function handleError(response) {
|
||||
const errorMessage = '<div>Preview failed to load!</div>';
|
||||
|
@ -42,7 +41,6 @@ function getPreview(body, anonymous, previewTab, isImage = false) {
|
|||
.then(handleError)
|
||||
.then(data => {
|
||||
previewTab.innerHTML = data;
|
||||
filterNode(previewTab);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
import { clearEl } from './dom';
|
||||
import store from './store';
|
||||
|
||||
function showVideoThumb(img) {
|
||||
const size = img.dataset.size;
|
||||
const uris = JSON.parse(img.dataset.uris);
|
||||
const thumbUri = uris[size];
|
||||
|
||||
const vidEl = img.querySelector('video');
|
||||
if (!vidEl) return false;
|
||||
|
||||
const imgEl = img.querySelector('img');
|
||||
if (!imgEl || imgEl.classList.contains('hidden')) return false;
|
||||
|
||||
imgEl.classList.add('hidden');
|
||||
|
||||
vidEl.innerHTML = `
|
||||
<source src="${thumbUri}" type="video/webm"/>
|
||||
<source src="${thumbUri.replace(/webm$/, 'mp4')}" type="video/mp4"/>
|
||||
`;
|
||||
vidEl.classList.remove('hidden');
|
||||
vidEl.play();
|
||||
|
||||
img.querySelector('.js-spoiler-info-overlay').classList.add('hidden');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function showThumb(img) {
|
||||
const size = img.dataset.size;
|
||||
const uris = JSON.parse(img.dataset.uris);
|
||||
const thumbUri = uris[size].replace(/webm$/, 'gif');
|
||||
|
||||
const picEl = img.querySelector('picture');
|
||||
if (!picEl) return showVideoThumb(img);
|
||||
|
||||
const imgEl = picEl.querySelector('img');
|
||||
if (!imgEl || imgEl.src.indexOf(thumbUri) !== -1) return false;
|
||||
|
||||
if (store.get('serve_hidpi') && !thumbUri.endsWith('.gif')) {
|
||||
// Check whether the HiDPI option is enabled, and make an exception for GIFs due to their size
|
||||
imgEl.srcset = `${thumbUri} 1x, ${uris.medium} 2x`;
|
||||
}
|
||||
|
||||
imgEl.src = thumbUri;
|
||||
if (uris[size].indexOf('.webm') !== -1) {
|
||||
const overlay = img.querySelector('.js-spoiler-info-overlay');
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.innerHTML = 'WebM';
|
||||
}
|
||||
else {
|
||||
img.querySelector('.js-spoiler-info-overlay').classList.add('hidden');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function showBlock(img) {
|
||||
img.querySelector('.image-filtered').classList.add('hidden');
|
||||
const imageShowClasses = img.querySelector('.image-show').classList;
|
||||
imageShowClasses.remove('hidden');
|
||||
imageShowClasses.add('spoiler-pending');
|
||||
}
|
||||
|
||||
function hideVideoThumb(img, spoilerUri, reason) {
|
||||
const vidEl = img.querySelector('video');
|
||||
if (!vidEl) return;
|
||||
|
||||
const imgEl = img.querySelector('img');
|
||||
const imgOverlay = img.querySelector('.js-spoiler-info-overlay');
|
||||
if (!imgEl) return;
|
||||
|
||||
imgEl.classList.remove('hidden');
|
||||
imgEl.src = spoilerUri;
|
||||
imgOverlay.innerHTML = reason;
|
||||
imgOverlay.classList.remove('hidden');
|
||||
|
||||
clearEl(vidEl);
|
||||
vidEl.classList.add('hidden');
|
||||
vidEl.pause();
|
||||
}
|
||||
|
||||
function hideThumb(img, spoilerUri, reason) {
|
||||
const picEl = img.querySelector('picture');
|
||||
if (!picEl) return hideVideoThumb(img, spoilerUri, reason);
|
||||
|
||||
const imgEl = picEl.querySelector('img');
|
||||
const imgOverlay = img.querySelector('.js-spoiler-info-overlay');
|
||||
|
||||
if (!imgEl || imgEl.src.indexOf(spoilerUri) !== -1) return;
|
||||
|
||||
imgEl.srcset = '';
|
||||
imgEl.src = spoilerUri;
|
||||
imgOverlay.innerHTML = reason;
|
||||
imgOverlay.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function spoilerThumb(img, spoilerUri, reason) {
|
||||
hideThumb(img, spoilerUri, reason);
|
||||
|
||||
switch (window.booru.spoilerType) {
|
||||
case 'click':
|
||||
img.addEventListener('click', event => { if (showThumb(img)) event.preventDefault(); });
|
||||
img.addEventListener('mouseleave', () => hideThumb(img, spoilerUri, reason));
|
||||
break;
|
||||
case 'hover':
|
||||
img.addEventListener('mouseenter', () => showThumb(img));
|
||||
img.addEventListener('mouseleave', () => hideThumb(img, spoilerUri, reason));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function spoilerBlock(img, spoilerUri, reason) {
|
||||
const imgEl = img.querySelector('.image-filtered img');
|
||||
const imgReason = img.querySelector('.filter-explanation');
|
||||
|
||||
if (!imgEl) return;
|
||||
|
||||
imgEl.src = spoilerUri;
|
||||
imgReason.innerHTML = reason;
|
||||
|
||||
img.querySelector('.image-show').classList.add('hidden');
|
||||
img.querySelector('.image-filtered').classList.remove('hidden');
|
||||
}
|
||||
|
||||
export { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb };
|
|
@ -1,58 +0,0 @@
|
|||
import { escapeHtml } from './dom';
|
||||
import { getTag } from '../booru';
|
||||
|
||||
function unique(array) {
|
||||
return array.filter((a, b, c) => c.indexOf(a) === b);
|
||||
}
|
||||
|
||||
function sortTags(hidden, a, b) {
|
||||
// If both tags have a spoiler image, sort by images count desc (hidden) or asc (spoilered)
|
||||
if (a.spoiler_image_uri && b.spoiler_image_uri) {
|
||||
return hidden ? b.images - a.images : a.images - b.images;
|
||||
}
|
||||
// If neither has a spoiler image, sort by images count desc
|
||||
else if (!a.spoiler_image_uri && !b.spoiler_image_uri) {
|
||||
return b.images - a.images;
|
||||
}
|
||||
|
||||
// Tag with spoiler image takes precedence
|
||||
return a.spoiler_image_uri ? -1 : 1;
|
||||
}
|
||||
|
||||
function getHiddenTags() {
|
||||
return unique(window.booru.hiddenTagList)
|
||||
.map(tagId => getTag(tagId))
|
||||
.sort(sortTags.bind(null, true));
|
||||
}
|
||||
|
||||
function getSpoileredTags() {
|
||||
if (window.booru.spoilerType === 'off') return [];
|
||||
|
||||
return unique(window.booru.spoileredTagList)
|
||||
.filter(tagId => window.booru.ignoredTagList.indexOf(tagId) === -1)
|
||||
.map(tagId => getTag(tagId))
|
||||
.sort(sortTags.bind(null, false));
|
||||
}
|
||||
|
||||
function imageHitsTags(img, matchTags) {
|
||||
const imageTags = JSON.parse(img.dataset.imageTags);
|
||||
return matchTags.filter(t => imageTags.indexOf(t.id) !== -1);
|
||||
}
|
||||
|
||||
function imageHitsComplex(img, matchComplex) {
|
||||
return matchComplex.hitsImage(img);
|
||||
}
|
||||
|
||||
function displayTags(tags) {
|
||||
const mainTag = tags[0], otherTags = tags.slice(1);
|
||||
let list = escapeHtml(mainTag.name), extras;
|
||||
|
||||
if (otherTags.length > 0) {
|
||||
extras = otherTags.map(tag => escapeHtml(tag.name)).join(', ');
|
||||
list += `<span title="${extras}">, ${extras}</span>`;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export { getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags };
|
|
@ -16,8 +16,8 @@ import { setupComments } from './comment';
|
|||
import { setupDupeReports } from './duplicate_reports.js';
|
||||
import { setFingerprintCookie } from './fingerprint';
|
||||
import { setupGalleryEditing } from './galleries';
|
||||
import { initImagesClientside } from './imagesclientside';
|
||||
import { bindImageTarget } from './image_expansion';
|
||||
import { setupInteractions } from './interactions';
|
||||
import { setupEvents } from './misc';
|
||||
import { setupNotifications } from './notifications';
|
||||
import { setupPreviews } from './preview';
|
||||
|
@ -44,7 +44,7 @@ whenReady(() => {
|
|||
registerEvents();
|
||||
setupBurgerMenu();
|
||||
bindCaptchaLinks();
|
||||
initImagesClientside();
|
||||
setupInteractions();
|
||||
setupComments();
|
||||
setupDupeReports();
|
||||
setFingerprintCookie();
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
defmodule Philomena.Search.Evaluator do
|
||||
# TODO: rethink the necessity of this module.
|
||||
# Can we do this in elasticsearch instead?
|
||||
|
||||
def hits?(doc, %{bool: bool_query}) do
|
||||
must(doc, bool_query[:must]) and
|
||||
must(doc, bool_query[:filter]) and
|
||||
should(doc, bool_query[:should]) and
|
||||
not should(doc, bool_query[:must_not])
|
||||
end
|
||||
|
||||
def hits?(doc, %{range: range_query}) do
|
||||
[term] = Map.keys(range_query)
|
||||
doc_values = wrap(doc[atomify(term)])
|
||||
|
||||
range_query[term]
|
||||
|> Enum.all?(fn
|
||||
{:gt, query_val} ->
|
||||
Enum.any?(doc_values, &(&1 > query_val))
|
||||
|
||||
{:gte, query_val} ->
|
||||
Enum.any?(doc_values, &(&1 >= query_val))
|
||||
|
||||
{:lt, query_val} ->
|
||||
Enum.any?(doc_values, &(&1 < query_val))
|
||||
|
||||
{:lte, query_val} ->
|
||||
Enum.any?(doc_values, &(&1 <= query_val))
|
||||
end)
|
||||
end
|
||||
|
||||
def hits?(doc, %{fuzzy: fuzzy_query}) do
|
||||
[{term, %{value: query_val, fuzziness: fuzziness}}] = Enum.to_list(fuzzy_query)
|
||||
|
||||
wrap(doc[atomify(term)])
|
||||
|> Enum.any?(fn doc_val ->
|
||||
cond do
|
||||
fuzziness >= 1 ->
|
||||
levenshtein(query_val, doc_val) <= fuzziness
|
||||
|
||||
fuzziness >= 0 ->
|
||||
levenshtein(query_val, doc_val) <= trunc((1 - fuzziness) * byte_size(query_val))
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def hits?(doc, %{wildcard: wildcard_query}) do
|
||||
[{term, query_val}] = Enum.to_list(wildcard_query)
|
||||
query_re = wildcard_to_regex(query_val)
|
||||
|
||||
wrap(doc[atomify(term)])
|
||||
|> Enum.any?(&Regex.match?(query_re, &1 || ""))
|
||||
end
|
||||
|
||||
def hits?(doc, %{match_phrase: phrase_query}) do
|
||||
# This is wildly inaccurate but practically unavoidable as
|
||||
# there is no good reason to import a term stemmer
|
||||
[{term, query_val}] = Enum.to_list(phrase_query)
|
||||
|
||||
wrap(doc[atomify(term)])
|
||||
|> Enum.any?(&String.contains?(&1, query_val))
|
||||
end
|
||||
|
||||
def hits?(_doc, %{nested: _}) do
|
||||
# No way to tell without a wildly expensive database query
|
||||
false
|
||||
end
|
||||
|
||||
def hits?(doc, %{term: term_query}) do
|
||||
[{term, query_val}] = Enum.to_list(term_query)
|
||||
|
||||
wrap(doc[atomify(term)])
|
||||
|> Enum.member?(query_val)
|
||||
end
|
||||
|
||||
def hits?(doc, %{terms: terms_query}) do
|
||||
[{term, query_vals}] = Enum.to_list(terms_query)
|
||||
|
||||
wrap(doc[atomify(term)])
|
||||
|> Enum.any?(&Enum.member?(query_vals, &1))
|
||||
end
|
||||
|
||||
def hits?(_doc, %{match_all: %{}}), do: true
|
||||
def hits?(_doc, %{match_none: %{}}), do: false
|
||||
def hits?(doc, %{function_score: %{query: query}}), do: hits?(doc, query)
|
||||
|
||||
defp must(_doc, nil), do: true
|
||||
defp must(doc, queries) when is_list(queries), do: Enum.all?(queries, &hits?(doc, &1))
|
||||
defp must(doc, query), do: hits?(doc, query)
|
||||
|
||||
defp should(_doc, nil), do: false
|
||||
defp should(doc, queries) when is_list(queries), do: Enum.any?(queries, &hits?(doc, &1))
|
||||
defp should(doc, query), do: hits?(doc, query)
|
||||
|
||||
defp wrap(list) when is_list(list), do: list
|
||||
defp wrap(object), do: [object]
|
||||
|
||||
defp atomify(atom) when is_atom(atom), do: atom
|
||||
defp atomify(string) when is_binary(string), do: String.to_existing_atom(string)
|
||||
|
||||
def levenshtein(s1, s2) do
|
||||
{dist, _lookup} = levenshtein_lookup(s1, s2, %{}, 0)
|
||||
|
||||
dist
|
||||
end
|
||||
|
||||
defp levenshtein_lookup(s1, s2, lookup, times) do
|
||||
case lookup[{s1, s2}] do
|
||||
nil ->
|
||||
levenshtein_execute(s1, s2, lookup, times)
|
||||
|
||||
val ->
|
||||
{val, lookup}
|
||||
end
|
||||
end
|
||||
|
||||
# Avoid pursuing excessively time-consuming substrings
|
||||
defp levenshtein_execute(s1, s2, lookup, times) when times > 2,
|
||||
do: {max(byte_size(s1), byte_size(s2)), lookup}
|
||||
|
||||
defp levenshtein_execute("", s2, lookup, _times), do: {byte_size(s2), lookup}
|
||||
defp levenshtein_execute(s1, "", lookup, _times), do: {byte_size(s1), lookup}
|
||||
defp levenshtein_execute(s1, s1, lookup, _times), do: {0, lookup}
|
||||
|
||||
defp levenshtein_execute(s1, s2, lookup, times) do
|
||||
{deletion, lookup} = levenshtein_lookup(chop(s1), s2, lookup, times + 1)
|
||||
{insertion, lookup} = levenshtein_lookup(s1, chop(s2), lookup, times + 1)
|
||||
{substitution, lookup} = levenshtein_lookup(chop(s1), chop(s2), lookup, times + 1)
|
||||
|
||||
min =
|
||||
Enum.min([
|
||||
deletion + 1,
|
||||
insertion + 1,
|
||||
substitution + last_bytes_different?(s1, s2)
|
||||
])
|
||||
|
||||
lookup = Map.put(lookup, {s1, s2}, min)
|
||||
|
||||
{min, lookup}
|
||||
end
|
||||
|
||||
defp chop(str) when is_binary(str), do: binary_part(str, 0, byte_size(str) - 1)
|
||||
|
||||
defp last_bytes_different?(s1, s2)
|
||||
when binary_part(s1, byte_size(s1) - 1, 1) == binary_part(s2, byte_size(s2) - 1, 1),
|
||||
do: 0
|
||||
|
||||
defp last_bytes_different?(_s1, _s2), do: 1
|
||||
|
||||
defp wildcard_to_regex(input) do
|
||||
re =
|
||||
input
|
||||
# escape regex metacharacters
|
||||
|> String.replace(~r/([.+^$\[\]\\\(\){}|-])/, "\\\\\\1")
|
||||
# * -> .* (kleene star)
|
||||
|> String.replace(~r/([^\\]|[^\\](?:\\\\)+)\*/, "\\1.*")
|
||||
# * -> .* (kleene star)
|
||||
|> String.replace(~r/\A(?:\\\\)*\*/, ".*")
|
||||
# ? -> .? (concatenation/alternation)
|
||||
|> String.replace(~r/([^\\]|[^\\](?:\\\\)+)\?/, "\\1.?")
|
||||
# ? -> .? (concatenation/alternation)
|
||||
|> String.replace(~r/\A(?:\\\\)*\?/, ".?")
|
||||
|
||||
Regex.compile!("\\A#{re}\\z", "im")
|
||||
end
|
||||
end
|
|
@ -27,7 +27,6 @@ defmodule PhilomenaWeb.Image.CommentController do
|
|||
preload: [:tags]
|
||||
|
||||
plug :verify_authorized when action in [:show]
|
||||
plug PhilomenaWeb.FilterForcedUsersPlug when action in [:create, :edit, :update]
|
||||
|
||||
# Undo the previous private parameter screwery
|
||||
plug PhilomenaWeb.LoadCommentPlug, [param: "id", show_hidden: true] when action in [:show]
|
||||
|
|
|
@ -15,8 +15,6 @@ defmodule PhilomenaWeb.Image.FaveController do
|
|||
persisted: true,
|
||||
preload: [:tags]
|
||||
|
||||
plug PhilomenaWeb.FilterForcedUsersPlug
|
||||
|
||||
def create(conn, _params) do
|
||||
user = conn.assigns.current_user
|
||||
image = conn.assigns.image
|
||||
|
|
|
@ -15,8 +15,6 @@ defmodule PhilomenaWeb.Image.VoteController do
|
|||
persisted: true,
|
||||
preload: [:tags]
|
||||
|
||||
plug PhilomenaWeb.FilterForcedUsersPlug
|
||||
|
||||
def create(conn, params) do
|
||||
user = conn.assigns.current_user
|
||||
image = conn.assigns.image
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
defmodule PhilomenaWeb.FilterForcedUsersPlug do
|
||||
@moduledoc """
|
||||
Halts the request pipeline if the current image belongs to the conn's
|
||||
"forced filter".
|
||||
"""
|
||||
|
||||
import Phoenix.Controller
|
||||
import Plug.Conn
|
||||
alias Philomena.Search.String, as: SearchString
|
||||
alias Philomena.Search.Evaluator
|
||||
alias Philomena.Images.Query
|
||||
alias PhilomenaWeb.ImageView
|
||||
|
||||
def init(_opts) do
|
||||
[]
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
maybe_fetch_forced(conn, conn.assigns.forced_filter)
|
||||
end
|
||||
|
||||
defp maybe_fetch_forced(conn, nil), do: conn
|
||||
|
||||
defp maybe_fetch_forced(conn, forced) do
|
||||
maybe_halt(conn, matches_filter?(conn.assigns.current_user, conn.assigns.image, forced))
|
||||
end
|
||||
|
||||
defp maybe_halt(conn, false), do: conn
|
||||
|
||||
defp maybe_halt(conn, true) do
|
||||
conn
|
||||
|> put_flash(:error, "You have been blocked from performing this action on this image.")
|
||||
|> redirect(external: conn.assigns.referrer)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp matches_filter?(user, image, filter) do
|
||||
matches_tag_filter?(image, filter.hidden_tag_ids) or
|
||||
matches_complex_filter?(user, image, filter.hidden_complex_str)
|
||||
end
|
||||
|
||||
defp matches_tag_filter?(image, tag_ids) do
|
||||
image.tags
|
||||
|> MapSet.new(& &1.id)
|
||||
|> MapSet.intersection(MapSet.new(tag_ids))
|
||||
|> Enum.any?()
|
||||
end
|
||||
|
||||
defp matches_complex_filter?(user, image, search_string) do
|
||||
image
|
||||
|> ImageView.image_filter_data()
|
||||
|> Evaluator.hits?(compile_filter(user, search_string))
|
||||
end
|
||||
|
||||
defp compile_filter(user, search_string) do
|
||||
case Query.compile(user, SearchString.normalize(search_string)) do
|
||||
{:ok, query} -> query
|
||||
_error -> %{match_all: %{}}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
- size = assigns[:size] || :full
|
||||
|
||||
= content_tag :div, [data: image_container_data(@conn, @image, size), class: "image-show-container"] do
|
||||
= content_tag :div, [class: "image-show-container"] do
|
||||
.block.block--fixed.block--warning.block--no-margin.image-filtered.hidden
|
||||
strong
|
||||
= link("This image is blocked by your current filter - click here to display it anyway", to: "#", data: [click_unfilter: @image.id])
|
||||
|
|
|
@ -123,31 +123,8 @@ defmodule PhilomenaWeb.ImageView do
|
|||
Application.get_env(:philomena, :image_url_root)
|
||||
end
|
||||
|
||||
def image_container_data(conn, image, size) do
|
||||
[
|
||||
image_id: image.id,
|
||||
image_tags: Jason.encode!(Enum.map(image.tags, & &1.id)),
|
||||
image_tag_aliases: image.tag_list_plus_alias_cache,
|
||||
score: image.score,
|
||||
faves: image.faves_count,
|
||||
upvotes: image.upvotes_count,
|
||||
downvotes: image.downvotes_count,
|
||||
comment_count: image.comments_count,
|
||||
created_at: NaiveDateTime.to_iso8601(image.created_at),
|
||||
source_url: image.source_url,
|
||||
uris: Jason.encode!(thumb_urls(image, can?(conn, :show, image))),
|
||||
width: image.image_width,
|
||||
height: image.image_height,
|
||||
aspect_ratio: image.image_aspect_ratio,
|
||||
size: size
|
||||
]
|
||||
end
|
||||
|
||||
def image_container(conn, image, size, block) do
|
||||
content_tag(:div, block.(),
|
||||
class: "image-container #{size}",
|
||||
data: image_container_data(conn, image, size)
|
||||
)
|
||||
def image_container(_conn, _image, size, block) do
|
||||
content_tag(:div, block.(), class: "image-container #{size}")
|
||||
end
|
||||
|
||||
def display_order(tags) do
|
||||
|
@ -216,26 +193,6 @@ defmodule PhilomenaWeb.ImageView do
|
|||
defp thumb_format(_, :rendered, _download), do: "png"
|
||||
defp thumb_format(format, _name, _download), do: format
|
||||
|
||||
def image_filter_data(image) do
|
||||
%{
|
||||
id: image.id,
|
||||
"namespaced_tags.name": String.split(image.tag_list_plus_alias_cache || "", ", "),
|
||||
score: image.score,
|
||||
faves: image.faves_count,
|
||||
upvotes: image.upvotes_count,
|
||||
downvotes: image.downvotes_count,
|
||||
comment_count: image.comments_count,
|
||||
created_at: image.created_at,
|
||||
first_seen_at: image.first_seen_at,
|
||||
source_url: image.source_url,
|
||||
width: image.image_width,
|
||||
height: image.image_height,
|
||||
aspect_ratio: image.image_aspect_ratio,
|
||||
sha512_hash: image.image_sha512_hash,
|
||||
orig_sha512_hash: image.image_orig_sha512_hash
|
||||
}
|
||||
end
|
||||
|
||||
def filter_or_spoiler_value(conn, image) do
|
||||
spoilered(conn)[image.id]
|
||||
end
|
||||
|
|
|
@ -24,11 +24,6 @@ defmodule PhilomenaWeb.LayoutView do
|
|||
host |> to_string
|
||||
end
|
||||
|
||||
defp ignored_tag_list(nil), do: []
|
||||
defp ignored_tag_list([]), do: []
|
||||
defp ignored_tag_list([{tag, _body, _dnp_entries}]), do: [tag.id]
|
||||
defp ignored_tag_list(tags), do: Enum.map(tags, & &1.id)
|
||||
|
||||
def clientside_data(conn) do
|
||||
conn = Conn.fetch_cookies(conn)
|
||||
|
||||
|
@ -40,9 +35,7 @@ defmodule PhilomenaWeb.LayoutView do
|
|||
data = [
|
||||
filter_id: filter.id,
|
||||
hidden_tag_list: Jason.encode!(filter.hidden_tag_ids),
|
||||
hidden_filter: Philomena.Search.String.normalize(filter.hidden_complex_str || ""),
|
||||
spoilered_tag_list: Jason.encode!(filter.spoilered_tag_ids),
|
||||
spoilered_filter: Philomena.Search.String.normalize(filter.spoilered_complex_str || ""),
|
||||
user_id: if(user, do: user.id, else: nil),
|
||||
user_name: if(user, do: user.name, else: nil),
|
||||
user_slug: if(user, do: user.slug, else: nil),
|
||||
|
@ -53,7 +46,6 @@ defmodule PhilomenaWeb.LayoutView do
|
|||
fancy_tag_edit: if(user, do: user.fancy_tag_field_on_edit, else: true),
|
||||
fancy_tag_upload: if(user, do: user.fancy_tag_field_on_upload, else: true),
|
||||
interactions: Jason.encode!(interactions),
|
||||
ignored_tag_list: Jason.encode!(ignored_tag_list(conn.assigns[:tags])),
|
||||
hide_staff_tools: conn.cookies["hide_staff_tools"]
|
||||
]
|
||||
|
||||
|
|
Loading…
Reference in a new issue