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 { $ } 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) {
|
function unmarshal(data) {
|
||||||
try { return JSON.parse(data); } catch (_) { return data; }
|
try { return JSON.parse(data); } catch (_) { return data; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBooruData() {
|
export function loadBooruData() {
|
||||||
const booruData = document.querySelector('.js-datastore').dataset;
|
const booruData = document.querySelector('.js-datastore').dataset;
|
||||||
|
|
||||||
// Assign all elements to booru because lazy
|
// Assign all elements to booru because lazy
|
||||||
|
@ -92,22 +12,8 @@ function loadBooruData() {
|
||||||
window.booru[prop] = unmarshal(booruData[prop]);
|
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
|
// CSRF
|
||||||
window.booru.csrfToken = $('meta[name="csrf-token"]').content;
|
window.booru.csrfToken = $('meta[name="csrf-token"]').content;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BooruOnRails() {
|
window.booru = {};
|
||||||
this.apiEndpoint = '/api/v2/';
|
|
||||||
this.hiddenTag = '/images/tagblocked.svg';
|
|
||||||
this.tagsVersion = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.booru = new BooruOnRails();
|
|
||||||
|
|
||||||
export { getTag, loadBooruData };
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
import { $, $$ } from './utils/dom';
|
import { $, $$ } from './utils/dom';
|
||||||
import { fetchHtml, handleError } from './utils/requests';
|
import { fetchHtml, handleError } from './utils/requests';
|
||||||
import { showBlock } from './utils/image';
|
|
||||||
import { addTag } from './tagsinput';
|
import { addTag } from './tagsinput';
|
||||||
|
|
||||||
// Event types and any qualifying conditions - return true to not run action
|
// Event types and any qualifying conditions - return true to not run action
|
||||||
|
@ -72,10 +71,7 @@ const actions = {
|
||||||
.catch(() => newTab.textContent = 'Error!');
|
.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
|
// Use this function to apply a callback to elements matching the selectors
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
import { $ } from './utils/dom';
|
import { $ } from './utils/dom';
|
||||||
import { showOwnedComments } from './communications/comment';
|
import { showOwnedComments } from './communications/comment';
|
||||||
import { filterNode } from './imagesclientside';
|
|
||||||
import { fetchHtml } from './utils/requests';
|
import { fetchHtml } from './utils/requests';
|
||||||
|
|
||||||
function handleError(response) {
|
function handleError(response) {
|
||||||
|
@ -94,9 +93,6 @@ function insertParentPost(data, clickedLink, fullComment) {
|
||||||
// Add class active_reply_link to the clicked link
|
// Add class active_reply_link to the clicked link
|
||||||
clickedLink.classList.add('active_reply_link');
|
clickedLink.classList.add('active_reply_link');
|
||||||
|
|
||||||
// Filter images (if any) in the loaded comment
|
|
||||||
filterNode(fullComment.previousSibling);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearParentPost(clickedLink, fullComment) {
|
function clearParentPost(clickedLink, fullComment) {
|
||||||
|
@ -125,9 +121,6 @@ function displayComments(container, commentsHtml) {
|
||||||
// Execute timeago on comments
|
// Execute timeago on comments
|
||||||
window.booru.timeAgo(document.getElementsByTagName('time'));
|
window.booru.timeAgo(document.getElementsByTagName('time'));
|
||||||
|
|
||||||
// Filter images in the comments
|
|
||||||
filterNode(container);
|
|
||||||
|
|
||||||
// Show options on own comments
|
// Show options on own comments
|
||||||
showOwnedComments();
|
showOwnedComments();
|
||||||
|
|
||||||
|
@ -171,7 +164,6 @@ function setupComments() {
|
||||||
loadComments(false);
|
loadComments(false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
filterNode(comments);
|
|
||||||
showOwnedComments();
|
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 { fetchJson } from './utils/requests';
|
||||||
import { filterNode } from './imagesclientside';
|
|
||||||
|
|
||||||
function handleError(response) {
|
function handleError(response) {
|
||||||
const errorMessage = '<div>Preview failed to load!</div>';
|
const errorMessage = '<div>Preview failed to load!</div>';
|
||||||
|
@ -42,7 +41,6 @@ function getPreview(body, anonymous, previewTab, isImage = false) {
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
previewTab.innerHTML = data;
|
previewTab.innerHTML = data;
|
||||||
filterNode(previewTab);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,4 +81,4 @@ function setupPreviews() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupPreviews };
|
export { setupPreviews };
|
||||||
|
|
|
@ -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 { setupDupeReports } from './duplicate_reports.js';
|
||||||
import { setFingerprintCookie } from './fingerprint';
|
import { setFingerprintCookie } from './fingerprint';
|
||||||
import { setupGalleryEditing } from './galleries';
|
import { setupGalleryEditing } from './galleries';
|
||||||
import { initImagesClientside } from './imagesclientside';
|
|
||||||
import { bindImageTarget } from './image_expansion';
|
import { bindImageTarget } from './image_expansion';
|
||||||
|
import { setupInteractions } from './interactions';
|
||||||
import { setupEvents } from './misc';
|
import { setupEvents } from './misc';
|
||||||
import { setupNotifications } from './notifications';
|
import { setupNotifications } from './notifications';
|
||||||
import { setupPreviews } from './preview';
|
import { setupPreviews } from './preview';
|
||||||
|
@ -44,7 +44,7 @@ whenReady(() => {
|
||||||
registerEvents();
|
registerEvents();
|
||||||
setupBurgerMenu();
|
setupBurgerMenu();
|
||||||
bindCaptchaLinks();
|
bindCaptchaLinks();
|
||||||
initImagesClientside();
|
setupInteractions();
|
||||||
setupComments();
|
setupComments();
|
||||||
setupDupeReports();
|
setupDupeReports();
|
||||||
setFingerprintCookie();
|
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]
|
preload: [:tags]
|
||||||
|
|
||||||
plug :verify_authorized when action in [:show]
|
plug :verify_authorized when action in [:show]
|
||||||
plug PhilomenaWeb.FilterForcedUsersPlug when action in [:create, :edit, :update]
|
|
||||||
|
|
||||||
# Undo the previous private parameter screwery
|
# Undo the previous private parameter screwery
|
||||||
plug PhilomenaWeb.LoadCommentPlug, [param: "id", show_hidden: true] when action in [:show]
|
plug PhilomenaWeb.LoadCommentPlug, [param: "id", show_hidden: true] when action in [:show]
|
||||||
|
|
|
@ -15,8 +15,6 @@ defmodule PhilomenaWeb.Image.FaveController do
|
||||||
persisted: true,
|
persisted: true,
|
||||||
preload: [:tags]
|
preload: [:tags]
|
||||||
|
|
||||||
plug PhilomenaWeb.FilterForcedUsersPlug
|
|
||||||
|
|
||||||
def create(conn, _params) do
|
def create(conn, _params) do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
image = conn.assigns.image
|
image = conn.assigns.image
|
||||||
|
|
|
@ -15,8 +15,6 @@ defmodule PhilomenaWeb.Image.VoteController do
|
||||||
persisted: true,
|
persisted: true,
|
||||||
preload: [:tags]
|
preload: [:tags]
|
||||||
|
|
||||||
plug PhilomenaWeb.FilterForcedUsersPlug
|
|
||||||
|
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
user = conn.assigns.current_user
|
user = conn.assigns.current_user
|
||||||
image = conn.assigns.image
|
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
|
- 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
|
.block.block--fixed.block--warning.block--no-margin.image-filtered.hidden
|
||||||
strong
|
strong
|
||||||
= link("This image is blocked by your current filter - click here to display it anyway", to: "#", data: [click_unfilter: @image.id])
|
= 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)
|
Application.get_env(:philomena, :image_url_root)
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_container_data(conn, image, size) do
|
def image_container(_conn, _image, size, block) do
|
||||||
[
|
content_tag(:div, block.(), class: "image-container #{size}")
|
||||||
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)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_order(tags) do
|
def display_order(tags) do
|
||||||
|
@ -216,26 +193,6 @@ defmodule PhilomenaWeb.ImageView do
|
||||||
defp thumb_format(_, :rendered, _download), do: "png"
|
defp thumb_format(_, :rendered, _download), do: "png"
|
||||||
defp thumb_format(format, _name, _download), do: format
|
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
|
def filter_or_spoiler_value(conn, image) do
|
||||||
spoilered(conn)[image.id]
|
spoilered(conn)[image.id]
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,11 +24,6 @@ defmodule PhilomenaWeb.LayoutView do
|
||||||
host |> to_string
|
host |> to_string
|
||||||
end
|
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
|
def clientside_data(conn) do
|
||||||
conn = Conn.fetch_cookies(conn)
|
conn = Conn.fetch_cookies(conn)
|
||||||
|
|
||||||
|
@ -40,9 +35,7 @@ defmodule PhilomenaWeb.LayoutView do
|
||||||
data = [
|
data = [
|
||||||
filter_id: filter.id,
|
filter_id: filter.id,
|
||||||
hidden_tag_list: Jason.encode!(filter.hidden_tag_ids),
|
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_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_id: if(user, do: user.id, else: nil),
|
||||||
user_name: if(user, do: user.name, else: nil),
|
user_name: if(user, do: user.name, else: nil),
|
||||||
user_slug: if(user, do: user.slug, 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_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),
|
fancy_tag_upload: if(user, do: user.fancy_tag_field_on_upload, else: true),
|
||||||
interactions: Jason.encode!(interactions),
|
interactions: Jason.encode!(interactions),
|
||||||
ignored_tag_list: Jason.encode!(ignored_tag_list(conn.assigns[:tags])),
|
|
||||||
hide_staff_tools: conn.cookies["hide_staff_tools"]
|
hide_staff_tools: conn.cookies["hide_staff_tools"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue