// ==UserScript== // @name IndexedAutocomplete // @namespace https://github.com/BrokenEagle/JavaScripts // @version 29.39 // @description Uses Indexed DB for autocomplete, plus caching of other data. // @source https://danbooru.donmai.us/users/23799 // @author BrokenEagle // @match https://*.donmai.us/* // @exclude /^https://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/ // @run-at document-idle // @downloadURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/IndexedAutocomplete.user.js // @updateURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/IndexedAutocomplete.user.js // @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js // @require https://cdn.jsdelivr.net/npm/localforage-removeitems@1.4.0/dist/localforage-removeitems.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/module.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/debug.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/utility.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/validate.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/storage.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/concurrency.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/statistics.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/network.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/danbooru.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/load.js // @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/menu.js // ==/UserScript== /* global JSPLib $ Danbooru validate */ /****Library updates****/ JSPLib.utility.renderTemplate = function (literals, args, mapping) { let output = ""; for (let i = 0; i < literals.raw.length; i++) { output += literals.raw[i]; if (i < args.length) { let insert = (mapping && args[i] in mapping ? mapping[args[i]] : args[i]); output += insert; } } return output; }; JSPLib.utility.generateTemplate = function (func, literals, args) { return function (mapping) { return func(JSPLib.utility.renderTemplate(literals, args, mapping)); }; }; JSPLib.utility.normalizeHTML = function ({template = false} = {}) { const normalize = function (output) { // Mark all of the spaces surrounded by a gt/lt and a non-space or lt/gt. These spaces need to stay. let marked_output = output.replaceAll('> <', '>\xff<').replace(/(?<=>)\s(?=[^ <])/g, '\xff').replace(/(?<=[^ >])\s(?=<)/g, '\xff'); let normalized_output = marked_output.replace(/\s+/g, ' ').replace(/(?<=>)\s/g, "").replace(/\s(?=<)/g, ""); // Once the HTML has been normalized, restore all of the intentional spaces. return normalized_output.replaceAll('\xff', ' '); }; return function (literals, ...args) { if (template) { return JSPLib.utility.generateTemplate(normalize, literals, args); } return normalize(JSPLib.utility.renderTemplate(literals, args)); }; }; /****Global variables****/ //Exterior script variables const DANBOORU_TOPIC_ID = '14701'; //Variables for load.js const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery', 'window.Danbooru', 'Danbooru.Autocomplete', 'Danbooru.CurrentUser']; const PROGRAM_LOAD_REQUIRED_SELECTORS = ['#top', '#page']; //Program name constants const PROGRAM_SHORTCUT = 'iac'; const PROGRAM_NAME = 'IndexedAutocomplete'; //Program data constants const PROGRAM_DATA_REGEX = /^(ac|pl|us|fg|ss|ar|wp|ft)-/; //Regex that matches the prefix of all program cache data const PROGRAM_DATA_KEY = { tag: 'ac', pool: 'pl', user: 'us', artist: 'ar', wiki: 'wp', forum: 'ft', saved_search: 'ss', favorite_group: 'fg', }; //Main program variable const IAC = {}; //For factory reset const LOCALSTORAGE_KEYS = [ 'iac-choice-info', ]; const PROGRAM_RESET_KEYS = { choice_order: {}, choice_data: {}, source_data: {}, }; //Available setting values const TAG_SOURCES = ['metatag', 'tag', 'tag-word', 'tag-abbreviation', 'tag-alias', 'tag-correction', 'tag-other-name']; const SCALE_TYPES = ['linear', 'square_root', 'logarithmic']; //Main settings const SETTINGS_CONFIG = { usage_multiplier: { reset: 0.9, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0, 1.0), hint: "Valid values: 0.0 - 1.0." }, usage_maximum: { reset: 20, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0), hint: "Set to 0 for no maximum." }, usage_expires: { reset: 2, parse: parseInt, validate: (data) => JSPLib.menu.validateNumber(data, true, 1), hint: "Number of days." }, usage_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Uncheck to turn off usage mechanism." }, alternate_sorting_enabled: { reset: false, validate: JSPLib.utility.isBoolean, hint: "Check to use alternate weights and/or scales for sorting calculations." }, postcount_scale: { allitems: SCALE_TYPES, reset: ['linear'], validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'radio', SCALE_TYPES), hint: "Select the type of scaling to be applied to the post count." }, exact_source_weight: { reset: 1.0, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0, 1.0), hint: "Valid values: 0.0 - 1.0." }, prefix_source_weight: { reset: 0.8, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0, 1.0), hint: "Valid values: 0.0 - 1.0." }, alias_source_weight: { reset: 0.2, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0, 1.0), hint: "Valid values: 0.0 - 1.0." }, correct_source_weight: { reset: 0.1, parse: parseFloat, validate: (data) => JSPLib.menu.validateNumber(data, false, 0.0, 1.0), hint: "Valid values: 0.0 - 1.0." }, metatag_source_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Adds metatags to autocomplete results on all post tag search inputs." }, BUR_source_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Adds BUR script elements to autocomplete results on bulk update requests, tag aliases, and tag implications." }, source_results_returned: { reset: 10, parse: parseInt, validate: (data) => JSPLib.menu.validateNumber(data, true, 5, 20), hint: "Number of results to return (5 - 20)." }, source_highlight_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Adds highlights and stylings to the HTML classes set by the program." }, highlight_words_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Underlines word matches on word match results." }, source_grouping_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Groups the results by tag autocomplete sources." }, source_order: { allitems: TAG_SOURCES, reset: TAG_SOURCES, sortvalue: true, validate: (data) => JSPLib.utility.arrayEquals(data, TAG_SOURCES), hint: "Used when source grouping is enabled. Drag and drop the sources to determine the group order." }, alternate_tag_source: { reset: false, validate: JSPLib.utility.isBoolean, hint: "Uses the /tags controller instead of the normal autocomplete source." }, alternate_tag_wildcards: { reset: false, validate: JSPLib.utility.isBoolean, hint: "Allows using a wildcard anywhere in a string with a wildcard always being added to the end." }, word_start_matches: { reset: false, validate: JSPLib.utility.isBoolean, hint: "Always adds a wildcard to the end, which forces the old behavior of searching from the beginning only. Note: This will cause correction results from being returned." }, network_only_mode: { reset: false, validate: JSPLib.utility.isBoolean, hint: `Always goes to network. Warning: This negates the benefit of cached data!` }, recheck_data_interval: { reset: 1, parse: parseInt, validate: (data) => JSPLib.menu.validateNumber(data, true, 0, 3), hint: "Number of days (0 - 3). Data expiring within this period gets automatically requeried. Setting to 0 disables this." }, text_input_autocomplete_enabled: { reset: true, validate: JSPLib.utility.isBoolean, hint: "Enables autocomplete in non-autocomplete text fields (Alt+A to enable/disable), inserting a wiki link upon completion." }, }; //Available config values const ALL_SOURCE_TYPES = ['indexed_db', 'local_storage']; const ALL_DATA_TYPES = ['tag', 'pool', 'user', 'artist', 'wiki', 'forum', 'saved_search', 'favorite_group', 'related_tag', 'custom']; const ALL_RELATED = ["", 'general', 'copyright', 'character', 'artist']; const CONTROL_CONFIG = { cache_info: { value: "Click to populate", hint: "Calculates the cache usage of the program and compares it to the total usage.", }, purge_cache: { display: `Purge cache (...)`, value: "Click to purge", hint: `Dumps all of the cached data related to ${PROGRAM_NAME}.`, }, data_source: { allitems: ALL_SOURCE_TYPES, value: 'indexed_db', hint: "Indexed DB is Cache Data and Local Storage is Program Data.", }, data_type: { allitems: ALL_DATA_TYPES, value: 'tag', hint: "Select type of data. Use Custom for querying by keyname.", }, related_tag_type: { allitems: ALL_RELATED, value: "", hint: "Select type of related tag data. Blank selects uncategorized data.", }, raw_data: { value: false, hint: "Select to import/export all program data", }, data_name: { value: "", buttons: ['get', 'save', 'delete', 'list', 'refresh'], hint: "Click Get to see the data, Save to edit it, and Delete to remove it.
List shows keys in their raw format, and Refresh checks the keys again.", }, }; const MENU_CONFIG = { topic_id: DANBOORU_TOPIC_ID, settings: [{ name: 'general', }, { name: 'source', }, { name: 'usage', message: "How items get sorted that are selected by the user.", }, { name: 'display', message: "Affects the presentation of autocomplete data to the user.", }, { name: 'sort', message: "Affects the order of tag autocomplete data.", }, { name: 'network', }], controls: [], }; // Default values const DEFAULT_VALUES = PROGRAM_RESET_KEYS; //Pre-CSS/HTML constants const BUR_TAG_CATEGORY = 400; const METATAG_TAG_CATEGORY = 500; //CSS Constants const PROGRAM_CSS = ` .iac-line-entry { display: flex; width: 100%; white-space: nowrap; align-items: center; } .iac-line-entry.ui-menu-item-wrapper.ui-state-active { border: none; } .iac-line-entry a { white-space: normal; } .iac-query > span:first-of-type, .iac-pool > span:first-of-type, .iac-favgroup > span:first-of-type, .iac-artist > span:first-of-type, .iac-forum-topic > span:first-of-type { flex-basis: 90%; } .iac-query > span:last-of-type, .iac-pool > span:last-of-type, .iac-favgroup > span:last-of-type, .iac-artist > span:last-of-type, .iac-forum-topic > span:last-of-type { flex-basis: 10%; text-align: right; } .iac-wiki-page > span:first-of-type { flex-basis: 85%; } .iac-wiki-page > span:last-of-type { flex-basis: 15%; text-align: right; } .iac-user .user-system, .iac-pool .pool-category-system, .iac-favgroup .favgroup-system, .iac-search .search-system { color: var(--text-color); } .iac-user > span, iac-search > span { flex-basis: 100%; } .iac-user-choice .autocomplete-item { padding-bottom: 1px; line-height: 150%; display: inline-block; } .iac-tag-alias a { font-style: italic; } .iac-tag-highlight > div:before { content: "●"; padding-right: 4px; font-weight: bold; font-size: 150%; transform: translateY(-2px); } } .iac-tag-bur > div:before { color: #000; } .iac-tag-exact > div:before { color: #DDD; } .iac-tag-word > div:before { color: #888; } .iac-tag-abbreviation > div:before { color: hotpink; } .iac-tag-alias > div:before { color: gold; } .iac-tag-autocorrect > div:before { color: cyan; } .iac-tag-other-name > div:before { color: orange; } .iac-tag-bur > div:before, .iac-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:link, .iac-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:visited, .iac-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:hover { color: #888; } .iac-highlight-match { font-weight: bold; } .related-tags .current-related-tags-columns li:before { content: "*"; font-family: monospace; font-weight: bold; visibility: hidden; padding-right: 0.2em; } .related-tags .current-related-tags-columns li.selected:before { visibility: visible; }`; const LIGHT_MODE_CSS = ` /****GENERAL****/ .iac-already-used { background-color: #FFFFAA; } .iac-user-choice .autocomplete-item { box-shadow: 0px 2px 0px #000; } .iac-tag-metatag > div:before, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover { color: #000; } .iac-highlight-match { filter: brightness(0.75); } /****FORUM****/ .ui-menu-item .forum-topic-category-0 { color: blue; } .ui-menu-item .forum-topic-category-1 { color: green; } .ui-menu-item .forum-topic-category-2 { color: red; }`; const DARK_MODE_CSS = ` /****GENERAL****/ .iac-already-used { background-color: #666622; } .iac-user-choice .autocomplete-item { box-shadow: 0px 2px 0px #DDD; } .iac-tag-metatag > div:before, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited, .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover { color: #FFF; } .iac-highlight-match { filter: brightness(1.25); } /****FORUM****/ .ui-menu-item .forum-topic-category-0 { color: var(--blue-3); } .ui-menu-item .forum-topic-category-1 { color: var(--green-3); } .ui-menu-item .forum-topic-category-2 { color: var(--red-3); }`; const SETTINGS_MENU_CSS = ` #indexed-autocomplete .jsplib-settings-grouping:not(#iac-general-settings) .iac-selectors label { width: 150px; } #indexed-autocomplete .iac-sortlist li { width: 10em; } #indexed-autocomplete .iac-formula { font-family: mono; }`; //HTML Constants const TEXT_AUTOCOMPLETE_DETAILS = JSPLib.utility.normalizeHTML()` `; const USAGE_SETTINGS_DETAILS = JSPLib.utility.normalizeHTML()`
Equations
`; const DISPLAY_SETTINGS_DETAILS = JSPLib.utility.normalizeHTML()` `; const SORT_SETTINGS_DETAILS = JSPLib.utility.normalizeHTML()`
Equations
`; const NETWORK_SETTINGS_DETAILS = JSPLib.utility.normalizeHTML()` `; const CACHE_DATA_DETAILS = JSPLib.utility.normalizeHTML()` `; const PROGRAM_DATA_DETAILS = JSPLib.utility.normalizeHTML()`

All timestamps are in milliseconds since the epoch (Epoch converter).

`; const AUTOCOMPLETE_MESSAGE = JSPLib.utility.normalizeHTML()` Autocomplete turned on!
Source: %s
Mode: %s
Capitalization: %s
`; const POOL_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'} ${'post_count'}
`; const USER_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'}
`; const FAVGROUP_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'} ${'post_count'}
`; const SEARCH_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'}
`; const WIKIPAGE_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'} ${'count'}
`; const ARTIST_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'} ${'count'}
`; const FORUMTOPIC_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
${'label'} ${'response_count'}
`; const LINE_ITEM_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})` ${'tag_html'} ${'post_text'}`; const LIST_ITEM_TEMPLATE = JSPLib.utility.normalizeHTML({template: true})`
  • ${'line_item'}
  • `; //Autocomplete constants const AUTOCOMPLETE_SOURCE = ['tag', 'wiki']; const AUTOCOMPLETE_MODE = ['tag', 'normal', 'pipe', 'custom']; const AUTOCOMPLETE_CAPITALIZATION = ['lowercase', 'uppercase', 'titlecase', 'propercase', 'exceptcase', 'romancase']; //Danbooru constants const COUNT_METATAGS = [ 'comment_count', 'deleted_comment_count', 'active_comment_count', 'note_count', 'deleted_note_count', 'active_note_count', 'flag_count', 'child_count', 'deleted_child_count', 'active_child_count', 'pool_count', 'deleted_pool_count', 'active_pool_count', 'series_pool_count', 'collection_pool_count', 'appeal_count', 'approval_count', 'replacement_count', ]; const COUNT_METATAG_SYNONYMS = COUNT_METATAGS.map((metatag) => { metatag = metatag.replace(/_count/, ""); if (metatag.match(/child$/)) { metatag = metatag.replace(/child$/, 'children'); } else { metatag += 's'; } return metatag; }); const CATEGORY_COUNT_METATAGS = ['gentags', 'arttags', 'copytags', 'chartags', 'metatags']; const ORDER_METATAGS = JSPLib.utility.multiConcat([ 'id', 'id_desc', 'md5', 'md5_asc', 'score', 'score_asc', 'upvotes', 'upvotes_asc', 'downvotes', 'downvotes_asc', 'favcount', 'favcount_asc', 'created_at', 'created_at_asc', 'change', 'change_asc', 'comment', 'comment_asc', 'comment_bumped', 'comment_bumped_asc', 'note', 'note_asc', 'artcomm', 'artcomm_asc', 'mpixels', 'mpixels_asc', 'portrait', 'landscape', 'filesize', 'filesize_asc', 'tagcount', 'tagcount_asc', 'duration', 'duration_asc', 'rank', 'curated', 'modqueue', 'random', 'custom', 'none', ], COUNT_METATAGS, COUNT_METATAG_SYNONYMS.flatMap((metatag) => [metatag, metatag + '_asc']), CATEGORY_COUNT_METATAGS.flatMap((metatag) => [metatag, metatag + '_asc']) ); const POST_STATUSES = ['active', 'deleted', 'pending', 'flagged', 'appealed', 'banned', 'modqueue', 'unmoderated']; const POST_RATINGS = ['general', 'sensitive', 'questionable', 'explicit']; const FILE_TYPES = ['jpg', 'png', 'gif', 'webp', 'avif', 'mp4', 'webm', 'swf', 'zip']; const DISAPPROVAL_REASONS = ['breaks_rules', 'poor_quality', 'disinterest']; const QUERY_METATAGS_MAP = { is: JSPLib.utility.multiConcat(['parent', 'child', 'sfw', 'nsfw'], POST_STATUSES, FILE_TYPES, POST_RATINGS), has: ['parent', 'children', 'source', 'appeals', 'flags', 'replacements', 'comments', 'commentary', 'notes', 'pools'], status: JSPLib.utility.concat(['any', 'all'], POST_STATUSES), child: JSPLib.utility.concat(['any', 'none'], POST_STATUSES), parent: JSPLib.utility.concat(['any', 'none'], POST_STATUSES), rating: POST_RATINGS, source: ['none', 'http'], embedded: ['true', 'false'], filetype: FILE_TYPES, commentary: ['all', 'none', 'true', 'false', 'translated', 'untranslated'], disapproved: DISAPPROVAL_REASONS, order: ORDER_METATAGS, }; const QUERY_STATIC_METATAGS = Object.keys(QUERY_METATAGS_MAP); const USER_METATAGS = [ 'user', 'approver', 'commenter', 'comm', 'noter', 'noteupdater', 'artcomm', 'commentaryupdater', 'flagger', 'appealer', 'upvote', 'downvote', 'fav', 'ordfav' ]; const POOL_METATAGS = [ 'pool', 'ordpool' ]; const FAVGROUP_METATAGS = [ 'favgroup', 'ordfavgroup' ]; const SAVED_SEARCH_METATAGS = [ 'search' ]; const QUERY_METATAGS = JSPLib.utility.multiConcat([ 'note', 'comment', 'id', 'source', 'md5', 'width', 'height', 'mpixels', 'ratio', 'score', 'upvotes', 'downvotes', 'favcount', 'filesize', 'date', 'age', 'limit', 'tagcount', 'pixiv_id', 'pixiv', 'unaliased', 'exif', 'duration', 'random', 'ai', ], QUERY_STATIC_METATAGS, USER_METATAGS, POOL_METATAGS, FAVGROUP_METATAGS, SAVED_SEARCH_METATAGS, COUNT_METATAGS, COUNT_METATAG_SYNONYMS, CATEGORY_COUNT_METATAGS).toSorted(); const QUERY_METATAG_REGEXES = { static: RegExp('^(?:' + QUERY_STATIC_METATAGS.join('|') + ')$'), user: RegExp('^(?:' + USER_METATAGS.join('|') + ')$'), pool: RegExp('^(?:' + POOL_METATAGS.join('|') + ')$'), favgroup: RegExp('^(?:' + FAVGROUP_METATAGS.join('|') + ')$'), search: RegExp('^(?:' + SAVED_SEARCH_METATAGS.join('|') + ')$'), tag: /^tag$/, }; const CATEGORY_NAMES = ['meta', 'general', 'character', 'copyright', 'artist']; const CATEGORIZATION_METATAGS = JSPLib.utility.concat(['ch', 'co', 'gen', 'char', 'copy', 'art'], CATEGORY_NAMES); const EDIT_METATAGS_MAP = { status: ['active', 'banned', 'pending'], child: ['none'], parent: ['none'], rating: POST_RATINGS, source: ['none'], upvote: ['self'], downvote: ['self'], fav: ['self'], disapproved: DISAPPROVAL_REASONS, }; const EDIT_STATIC_METATAGS = Object.keys(EDIT_METATAGS_MAP); const EDIT_METATAGS = JSPLib.utility.multiConcat([ 'newpool', 'pool', 'favgroup', ], EDIT_STATIC_METATAGS, CATEGORIZATION_METATAGS).toSorted(); const EDIT_METATAG_REGEXES = Object.assign({}, QUERY_METATAG_REGEXES, { static: RegExp('^(?:' + EDIT_STATIC_METATAGS.join('|') + ')$'), user: RegExp('^(?:' + JSPLib.utility.arrayDifference(USER_METATAGS, ['upvote', 'downvote', 'fav']).join('|') + ')$') }) ; const CATEGORY_DATA = CATEGORY_NAMES.map((category) => ({ type: 'tag', label: category, name: category, post_count: 'N/A', source: 'category', category: BUR_TAG_CATEGORY, key: null, })); const USER_OPTION_METATAGS = [ 'flagger', 'appealer', 'commenter', 'comm', 'commentaryupdater', 'artcomm', 'noter', 'noteupdater', 'upvoter', 'upvote', 'downvoter', 'downvote', 'approver', 'user' ]; const FAVGROUP_OPTION_METATAGS = [ 'favgroup' ]; const POOL_OPTION_METATAGS = [ 'pool' ]; const SAVED_SEARCH_OPTION_METATAGS = [ 'search' ]; const ALL_OPTION_METATAGS = JSPLib.utility.multiConcat( USER_OPTION_METATAGS, FAVGROUP_OPTION_METATAGS, POOL_OPTION_METATAGS, SAVED_SEARCH_OPTION_METATAGS ); //Regex constants const TERM_REGEX = RegExp('([-~]*)(?:(' + JSPLib.utility.concat(QUERY_METATAGS, CATEGORIZATION_METATAGS).join('|') + '):)?(\\S*)$', 'i'); const CATEGORY_REGEX = RegExp('^[-~]*(?:' + CATEGORIZATION_METATAGS.join('|') + '):$', 'i'); const WORD_DELIMITERS = '_+:;!./()-'; const DELIMITER_GROUP = `[${WORD_DELIMITERS}]`; const DELIMITER_NOT_GROUP = `[^${WORD_DELIMITERS}]`; const DELIMITER_LOOKBEHIND = `(?<=^|${DELIMITER_GROUP})`; const DELIMITER_GROUP_RG = new RegExp(DELIMITER_GROUP); const ALL_DELIMTER_RG = new RegExp(DELIMITER_GROUP + '|' + DELIMITER_NOT_GROUP + '+', 'g'); //BUR constants const BUR_KEYWORDS = ['alias', 'unalias', 'imply', 'unimply', 'rename', 'update', 'deprecate', 'undeprecate', 'nuke', 'category', 'convert']; const BUR_DATA = BUR_KEYWORDS.map((tag) => ({ type: 'tag', label: tag, name: tag, post_count: 'BUR', source: 'bur', category: BUR_TAG_CATEGORY, key: null, })); const BUR_LIMITS = { alias: 4, unalias: 4, imply: 4, unimply: 4, rename: 4, deprecate: 2, undeprecate: 2, nuke: 2, category: 4, convert: 4, }; //Time constants const PRUNE_EXPIRES = JSPLib.utility.one_day; const JQUERY_DELAY = 500; //Delay for calling functions after initialization const TIMER_POLL_INTERVAL = 100; //Polling interval for checking program status //Data inclusion lists const ALL_CATEGORIES = [0, 1, 3, 4, 5]; const ALL_TOPICS = [0, 1, 2]; const ALL_POOLS = ['system', 'collection', 'series']; const ALL_USERS = ['system', 'Member', 'Gold', 'Platinum', 'Builder', 'Contributor', 'Approver', 'Moderator', 'Admin', 'Owner']; //All of the following are used to determine when to run the script const AUTOCOMPLETE_USERLIST = [ '[data-autocomplete=user]', ]; //DOM elements with race condition const AUTOCOMPLETE_REBINDLIST = [ '[data-autocomplete=tag-query]', '[data-autocomplete=tag-edit]', '.autocomplete-mentions textarea', ]; //DOM elements with autocomplete const AUTOCOMPLETE_DOMLIST = [ '#search_topic_title_matches', '#bulk_update_request_script', '[data-autocomplete=tag]', '[data-autocomplete=wiki-page]', '[data-autocomplete=artist]', '[data-autocomplete=pool]', '[data-autocomplete=saved-search-label]', '[data-autocomplete=forum-topic]', ].concat(AUTOCOMPLETE_REBINDLIST).concat(AUTOCOMPLETE_USERLIST); const AUTOCOMPLETE_ALL_SELECTORS = AUTOCOMPLETE_DOMLIST.join(','); const AUTOCOMPLETE_USER_SELECTORS = AUTOCOMPLETE_USERLIST.join(','); const AUTOCOMPLETE_REBIND_SELECTORS = AUTOCOMPLETE_REBINDLIST.join(','); const AUTOCOMPLETE_MULTITAG_SELECTORS = ['tag-query', 'tag-edit'].map((ac_type) => ['nav', 'page'].map((id_select) => `#${id_select} [data-autocomplete=${ac_type}]`).join(', ')).join(', '); //Expiration variables const EXPIRATION_CONFIG = { tag: { logarithmic_start: 100, minimum: JSPLib.utility.one_week, maximum: JSPLib.utility.one_month, }, pool: { logarithmic_start: 10, minimum: JSPLib.utility.one_week, maximum: JSPLib.utility.one_month, }, user: { minimum: JSPLib.utility.one_month, }, favgroup: { minimum: JSPLib.utility.one_week, }, search: { minimum: JSPLib.utility.one_week, }, wikipage: { logarithmic_start: 100, minimum: JSPLib.utility.one_week, maximum: JSPLib.utility.one_month, }, artist: { logarithmic_start: 10, minimum: JSPLib.utility.one_week, maximum: JSPLib.utility.one_month, }, forumtopic: { minimum: JSPLib.utility.one_week, }, }; //Source variables const SOURCE_KEY = { ac: 'tag', pl: 'pool', us: 'user', fg: 'favgroup', ss: 'search', ar: 'artist', wp: 'wikipage', ft: 'forumtopic' }; const SOURCE_CONFIG = { tag1: { url: 'autocomplete', invalid: (term) => ParseQuery(term, term.length).prefix.length > 0, data: (term, query_type) => { let type = query_type === 'tag' ? 'tag' : 'tag_query'; return { search: { type, query: term, } }; }, map: (tag) => ( { antecedent: tag.antecedent ?? null, name: tag.value, category: tag.category, post_count: tag.post_count, source: tag.type, } ), expiration: (d) => (d.length ? ExpirationTime('tag', d[0].post_count) : MinimumExpirationTime('tag')), search_start: true, spaces_allowed: false }, tag2: { url: 'tags', invalid: (term) => ParseQuery(term, term.length).prefix.length > 0, data: (term) => ( { search: { name_or_alias_matches: term, hide_empty: true, order: 'count', }, only: 'name,category,post_count,consequent_aliases[antecedent_name]', } ), map: (tag, term) => Object.assign({ name: tag.name, category: tag.category, post_count: tag.post_count, }, GetConsequentMatch(term, tag)) , expiration: (d) => (d.length ? ExpirationTime('tag', d[0].post_count) : MinimumExpirationTime('tag')), search_start: true, spaces_allowed: false }, metatag: {}, pool: { url: 'pools', data: (term) => ({ search: { order: 'post_count', name_matches: term }, only: 'name,category,post_count' }), map: (pool) => ({ name: pool.name, post_count: pool.post_count, category: pool.category }), expiration: (d) => (d.length ? ExpirationTime('pool', d[0].post_count) : MinimumExpirationTime('pool')), search_start: false, spaces_allowed: true, render: (_domobj, item) => { let merge = Object.assign({}, item, { post_count: (['any', 'none', 'collection', 'series'].includes(item.name) ? 'N/A' : item.post_count), }); return $(POOL_TEMPLATE(merge)); }, }, user: { url: 'users', data: (term) => ({ search: { order: 'post_upload_count', current_user_first: true, name_matches: term, }, only: 'name,level_string' }), map: (user) => ({ name: user.name, level: user.level_string }), expiration: () => MinimumExpirationTime('user'), search_start: true, spaces_allowed: false, render: (_domobj, item) => $(USER_TEMPLATE({level: item.level.toLowerCase(), label: item.label})), }, favgroup: { url: 'favorite_groups', data: (term) => ({ search: { order: 'post_count', name_matches: term, creator_id: IAC.user_id, }, only: 'name,post_ids' }), map: (favgroup) => ({ name: favgroup.name, post_count: favgroup.post_ids.length, category: 'danbooru', }), expiration: () => MinimumExpirationTime('favgroup'), search_start: false, spaces_allowed: true, render: (_domobj, item) => { let merge = Object.assign({}, item, { post_count: (item.category === 'system' ? 'N/A' : item.post_count), }); return $(FAVGROUP_TEMPLATE(merge)); }, }, search: { url: 'autocomplete', data: (term) => ({ search: { type: 'saved_search_label', query: term, } }), map: (label) => ({ name: label.value, category: 'danbooru', }), expiration: () => MinimumExpirationTime('search'), search_start: true, spaces_allowed: false, render: (_domobj, item) => $(SEARCH_TEMPLATE(item)), }, wikipage: { url: 'wiki_pages', data: (term) => ({ search: { order: 'post_count', hide_deleted: true, title_ilike: term.replace(/ /g, '_'), }, only: 'title,tag[category,post_count]' }), map: (wikipage) => ({ name: wikipage.title, category: wikipage.tag?.category || 0, post_count: wikipage.tag?.post_count || 0, no_tag: !wikipage.tag, }), expiration: (d) => (d.length && d[0].tag ? ExpirationTime('wikipage', d[0].tag.post_count) : MinimumExpirationTime('wikipage')), search_start: true, spaces_allowed: true, render: (_domobj, item) => { let count = (item.no_tag ? 'No tag' : item.post_count); return $(WIKIPAGE_TEMPLATE(Object.assign({count}, item))); }, }, artist: { url: 'artists', data: (term) => ({ search: { order: 'post_count', is_active: true, name_like: term.trim().replace(/\s+/g, '_') }, only: 'name,tag[post_count]' }), map: (artist) => ({ post_count: artist.tag?.post_count, name: artist.name, no_tag: !artist.tag, }), expiration: (d) => (d.length && d[0].tag ? ExpirationTime('artist', d[0].tag.post_count) : MinimumExpirationTime('artist')), search_start: true, spaces_allowed: false, render: (_domobj, item) => { let count = (item.no_tag ? 'No tag' : item.post_count); return $(ARTIST_TEMPLATE({label: item.label, count})); }, }, forumtopic: { url: 'forum_topics', data: (term) => ({ search: { order: 'sticky', title_ilike: term, }, only: 'title,category_id,response_count' }), map: (forumtopic) => ({ response_count: forumtopic.response_count, category: forumtopic.category_id, name: forumtopic.title, }), expiration: () => MinimumExpirationTime('forumtopic'), search_start: false, spaces_allowed: true, render: (_domobj, item) => $(FORUMTOPIC_TEMPLATE(item)), } }; //Validate constants const AUTOCOMPLETE_CONSTRAINTS = { entry: JSPLib.validate.arrayentry_constraints({maximum: 20}), tag: { antecedent: JSPLib.validate.stringnull_constraints, category: JSPLib.validate.inclusion_constraints(ALL_CATEGORIES.concat(METATAG_TAG_CATEGORY)), post_count: JSPLib.validate.postcount_constraints, name: JSPLib.validate.stringonly_constraints, source: JSPLib.validate.inclusion_constraints(TAG_SOURCES), }, get metatag() { return this.tag; }, pool: { category: JSPLib.validate.inclusion_constraints(ALL_POOLS), post_count: JSPLib.validate.counting_constraints, name: JSPLib.validate.stringonly_constraints, }, user: { level: JSPLib.validate.inclusion_constraints(ALL_USERS), name: JSPLib.validate.stringonly_constraints, }, favgroup: { post_count: JSPLib.validate.counting_constraints, name: JSPLib.validate.stringonly_constraints, category: JSPLib.validate.inclusion_constraints(['system', 'danbooru']), }, search: { name: JSPLib.validate.stringonly_constraints, category: JSPLib.validate.inclusion_constraints(['system', 'danbooru']), }, artist: { post_count: JSPLib.validate.counting_constraints, name: JSPLib.validate.stringonly_constraints, no_tag: JSPLib.validate.boolean_constraints, }, wikipage: { post_count: JSPLib.validate.counting_constraints, name: JSPLib.validate.stringonly_constraints, category: JSPLib.validate.inclusion_constraints(ALL_CATEGORIES), no_tag: JSPLib.validate.boolean_constraints, }, forumtopic: { response_count: JSPLib.validate.counting_constraints, name: JSPLib.validate.stringonly_constraints, category: JSPLib.validate.inclusion_constraints(ALL_TOPICS), }, }; const USAGE_CONSTRAINTS = { expires: JSPLib.validate.expires_constraints, use_count: { numericality: { greaterThanOrEqualTo: 0, }, }, }; /****Functions****/ //Validate functions function ValidateEntry(key, entry) { const printer = JSPLib.debug.getFunctionPrint('ValidateEntry'); if (!JSPLib.validate.validateIsHash(key, entry)) { return false; } if (key.match(/^(?:ac|pl|us|fg|ss|ar|wp|ft)-/)) { return ValidateAutocompleteEntry(key, entry); } printer.debuglog("Bad key!"); return false; } function ValidateAutocompleteEntry(key, entry) { if (!JSPLib.validate.validateHashEntries(key, entry, AUTOCOMPLETE_CONSTRAINTS.entry)) { return false; } let type = SOURCE_KEY[key.slice(0, 2)]; for (let i = 0; i < entry.value.length; i++) { if (!JSPLib.validate.validateHashEntries(`${key}.value[${i}]`, entry.value[i], AUTOCOMPLETE_CONSTRAINTS[type])) { return false; } } return true; } function ValidateProgramData(key, entry) { var checkerror = []; switch (key) { case 'iac-user-settings': checkerror = JSPLib.menu.validateUserSettings(entry, SETTINGS_CONFIG); break; case 'iac-prune-expires': if (!Number.isInteger(entry)) { checkerror = ["Value is not an integer."]; } break; case 'iac-choice-info': if (JSPLib.utility.isHash(entry)) { checkerror = ValidateUsageData(entry); } else { checkerror = ['Value is not a hash']; } break; case 'iac-ac-source': if (!Number.isInteger(entry) || entry > AUTOCOMPLETE_SOURCE.length || entry < 0) { checkerror = [`Value is not an integer between 0 and {AUTOCOMPLETE_SOURCE.length - 1}.`]; } break; case 'iac-ac-mode': if (!Number.isInteger(entry) || entry > AUTOCOMPLETE_MODE.length || entry < 0) { checkerror = [`Value is not an integer between 0 and {AUTOCOMPLETE_MODE.length - 1}.`]; } break; case 'iac-ac-caps': if (!Number.isInteger(entry) || entry > AUTOCOMPLETE_CAPITALIZATION.length || entry < 0) { checkerror = [`Value is not an integer between 0 and {AUTOCOMPLETE_CAPITALIZATION.length - 1}.`]; } break; default: checkerror = ["Not a valid program data key."]; } if (checkerror.length) { JSPLib.validate.outputValidateError(key, checkerror); return false; } return true; } //Scalpel validation... removes only data that is bad instead of tossing everything function ValidateUsageData(choice_info) { let error_messages = []; let choice_order = choice_info.choice_order; let choice_data = choice_info.choice_data; if (!JSPLib.utility.isHash(choice_order) || !JSPLib.utility.isHash(choice_data)) { error_messages.push("Choice data/order is not a hash."); choice_info.choice_order = {}; choice_info.choice_data = {}; return error_messages; } //Validate choice order for (let type in choice_order) { if (!Array.isArray(choice_order[type])) { error_messages.push(`choice_order[${type}] is not an array.`); delete choice_order[type]; continue; } for (let i = 0; i < choice_order[type].length; i++) { if (!JSPLib.utility.isString(choice_order[type][i])) { error_messages.push(`choice_order[${type}][${i}] is not a string`); choice_order[type].splice(i, 1); i--; } } } //Validate choice data for (let type in choice_data) { if (!JSPLib.utility.isHash(choice_data[type])) { error_messages.push(`choice_data[${type}] is not a hash`); delete choice_data[type]; continue; } for (let key in choice_data[type]) { let validator = Object.assign({}, AUTOCOMPLETE_CONSTRAINTS[type], USAGE_CONSTRAINTS); let check = validate(choice_data[type][key], validator); if (check !== undefined) { error_messages.push(`choice_data[${type}][${key}]`, check); delete choice_data[type][key]; continue; } let extra_keys = JSPLib.utility.arrayDifference(Object.keys(choice_data[type][key]), Object.keys(validator)); if (extra_keys.length) { error_messages.push(`Hash contains extra keys: ${type} - ${key}`, extra_keys); delete choice_data[type][key]; } } } //Validate same types between both let type_diff = JSPLib.utility.arraySymmetricDifference(Object.keys(choice_order), Object.keys(choice_data)); if (type_diff.length) { error_messages.push("Type difference between choice order and choice data:", type_diff); type_diff.forEach((type) => { delete choice_order[type]; delete choice_data[type]; }); } //Validate same keys between both for (let type in choice_order) { let key_diff = JSPLib.utility.arraySymmetricDifference(choice_order[type], Object.keys(choice_data[type])); if (key_diff.length) { error_messages.push("Key difference between choice order and choice data:", type, key_diff); key_diff.forEach((key) => { choice_order[type] = JSPLib.utility.arrayDifference(choice_order[type], [key]); delete choice_data[type][key]; }); } } return error_messages; } function ValidateCached(cached, type, term, word_mode) { if (!cached) return false; if (type !== 'tag') return true; if (word_mode) { return cached.value.every((item) => GetWordMatches(item.antecedent || item.name, term, false)); } return cached.value.every((item) => GetGlobMatches(item.antecedent || item.name, term, false)); } //Helper functions function GetQueryType(element) { if (['post_tag_string', 'tag-script-field'].includes(element.id)) { return 'tag-edit'; } let query_type = $(element).data('autocomplete'); return (query_type === 'tag-edit' ? 'tag-query' : query_type); } function ParseQuery(text, caret) { let before_caret_text = text.substring(0, caret); let match = before_caret_text.match(TERM_REGEX); let operator = match[1]; let metatag = match[2] ? match[2].toLowerCase() : "tag"; let term = match[3]; let prefix = operator; if (metatag !== 'tag') { prefix += metatag + ':'; } if (IAC.categories.includes(metatag)) { metatag = 'tag'; } return {operator, metatag, term, prefix}; } function RemoveTerm(str, index) { str = ' ' + str + ' '; let first_slice = str.slice(0, index); let second_slice = str.slice(index); let first_space = first_slice.lastIndexOf(' '); let second_space = second_slice.indexOf(' '); return (first_slice.slice(0, first_space) + second_slice.slice(second_space)).slice(1, -1); } function GetConstantMatches(constant_data, term) { let matching = []; let nonmatching = []; constant_data.forEach((data) => { if (GetGlobMatches(data.name, term)) { matching.push(data); } else { nonmatching.push(data); } }); return JSPLib.utility.concat(matching, nonmatching).map((data) => Object.assign({term}, data)); } function GetConsequentMatch(term, tag) { let retval = {source: 'tag', antecedent: null}; let regex = RegExp('^' + JSPLib.utility.regexpEscape(term).replace(/\\\*/g, '.*')); if (!tag.name.match(regex)) { let matching_consequent = tag.consequent_aliases.filter((consequent) => consequent.antecedent_name.match(regex)); if (matching_consequent.length) { retval = {source: 'tag-alias', antecedent: matching_consequent[0].antecedent_name}; } } return retval; } const MapMetatag = (type, metatag, value) => ({ type, antecedent: null, label: metatag + ':' + value, value: metatag + ':' + value, name: metatag + ':' + value, original: value, post_count: METATAG_TAG_CATEGORY, source: 'metatag', category: METATAG_TAG_CATEGORY }); function MetatagData(type) { MetatagData.memoized ??= {}; if (!MetatagData.memoized[type]) { let metatags = (type === 'tag-edit' ? EDIT_METATAGS : QUERY_METATAGS); MetatagData.memoized[type] = metatags .filter((tag) => (tag[0] !== '-')) .map((tag) => (MapMetatag('tag', tag, ""))); } return MetatagData.memoized[type]; } function SubmetatagData(type) { SubmetatagData.memoized ??= {}; if (!SubmetatagData.memoized[type]) { SubmetatagData.memoized[type] = []; let mapping = (type === 'tag-edit' ? EDIT_METATAGS_MAP : QUERY_METATAGS_MAP); for (let metatag in mapping) { for (let i = 0; i < mapping[metatag].length; i++) { SubmetatagData.memoized[type].push(MapMetatag('metatag', metatag, mapping[metatag][i])); } } } return SubmetatagData.memoized[type]; } function GlobRegex(search, use_capture, return_groups = false) { GlobRegex.regexes ||= {}; GlobRegex.capture_groups ||= {}; let key = search + '\xff' + use_capture; if (!(key in GlobRegex.regexes)) { const captureMap = ( use_capture ? (val) => (val.slice(0, 2) === String.raw`\*` ? '(.*)' : `(${val})`) : (val) => (val.slice(0, 2) === String.raw`\*` ? '.*' : `${val}`) ); GlobRegex.capture_groups[key] = JSPLib.utility.findAll(search, /\*|[^*]+/g) .filter((val) => val !== '') .map((val) => JSPLib.utility.regexpEscape(val)) .map(captureMap); GlobRegex.regexes[key] = new RegExp('^' + GlobRegex.capture_groups[key].join("") + '$', 'i'); } return (return_groups ? GlobRegex.capture_groups[key] : GlobRegex.regexes[key]); } function WordRegex(search, use_capture, return_groups = false) { WordRegex.regexes ||= {}; WordRegex.capture_groups ||= {}; let key = search + '\xff' + use_capture; if (!(key in WordRegex.regexes)) { let bookend = (use_capture ? '(.*)' : '.*'); let capture_groups = JSPLib.utility.findAll(search, ALL_DELIMTER_RG) .filter((val) => val !== '') .map((word) => { if (word.match(DELIMITER_GROUP_RG)) { return (use_capture ? '(.*)' : '.*'); } let escape_word = JSPLib.utility.regexpEscape(word); return DELIMITER_LOOKBEHIND + (use_capture ? `(${escape_word})` : escape_word); }); WordRegex.capture_groups[key] = [bookend, ...capture_groups, bookend]; WordRegex.regexes[key] = new RegExp(WordRegex.capture_groups[key].join("") + '(.*)', 'i'); } return (return_groups ? WordRegex.capture_groups[key] : WordRegex.regexes[key]); } //Get regex from separate function and memoize that value function GetGlobMatches(name, search, use_capture) { let regex = GlobRegex(search, use_capture); let match = name.match(regex); return match; } //Get regex from separate function and memoize that value function GetWordMatches(name, search, use_capture) { let regex = WordRegex(search, use_capture); let match = name.match(regex); return match; } //Time functions function MinimumExpirationTime(type) { return EXPIRATION_CONFIG[type].minimum; } function MaximumExpirationTime(type) { return (EXPIRATION_CONFIG[type].maximum ? EXPIRATION_CONFIG[type].maximum : EXPIRATION_CONFIG[type].minimum); } //Logarithmic increase of expiration time based upon a count function ExpirationTime(type, count) { let config = EXPIRATION_CONFIG[type]; let expiration = Math.log10(10 * count / config.logarithmic_start) * config.minimum; expiration = Math.max(expiration, config.minimum); expiration = Math.min(expiration, config.maximum); return Math.round(expiration); } //Render functions function AutocompleteRenderItem(list, item) { if ('html' in item) { return Danbooru.Autocomplete.render_item(list, item); } if (SOURCE_CONFIG[item.type].render) { return RenderListItem(SOURCE_CONFIG[item.type].render)(list, item); } let tag_html = ""; if (item.antecedent) { tag_html = `${item.label}`; let antecedent = item.antecedent.replace(/_/g, " "); tag_html = '' + tag_html; tag_html = `${antecedent}` + tag_html; } else { tag_html = `${item.label}`; } let post_text = ""; if (item.post_count !== undefined) { let count = item.post_count; post_text = String(item.post_count); if (count >= 1000000) { post_text = JSPLib.utility.setPrecision(count / 1000000, 2) + 'M'; } else if (count >= 1000) { post_text = JSPLib.utility.setPrecision(count / 1000, 2) + "k"; } } let url = '/posts?tags=' + encodeURIComponent(item.name); let link_classes = ['iac-autocomplete-link']; if (item.type === 'tag') { link_classes.push('tag-type-' + item.category); } else if (item.type === 'user') { link_classes.push('user-' + item.level.toLowerCase()); } else if (item.type === 'pool') { link_classes.push('pool-category-' + item.category); } let line_item = LINE_ITEM_TEMPLATE({url, link_classes: link_classes.join(' '), tag_html, post_text}); let data_items = ["type", "antecedent", "value", "category", "post_count"].map((attr) => `data-autocomplete-${attr.replace(/_/g, "-")}="${item[attr]}"`); let $list_item = $(LIST_ITEM_TEMPLATE({data_items: data_items.join(' '), line_item})); $list_item.data("item.autocomplete", item); $list_item.find('a').on(JSPLib.program_click, (event) => {event.preventDefault();}); $list_item.appendTo(list); HighlightSelected($list_item, item); return $list_item; } function RenderListItem(alink_func) { return function (list, item) { let $link = alink_func($(''), item); let $container = $('
    ').append($link); HighlightSelected($container, item); return $('
  • ').data('item.autocomplete', item).append($container).appendTo(list); }; } function RenderMenuItem(ul, items) { items.forEach((item) => { this._renderItemData(ul, item); }); } function RenderAutocompleteNotice(type, list, index) { let values = list.map((val, i) => (index === i ? `${val}` : val)); let line = values.join(' | '); return `Autocomplete ${type}: ${line}`; } //Main helper functions function CapitalizeAutocomplete(string) { switch (IAC.ac_caps) { case 1: return string.toUpperCase(); case 2: return JSPLib.utility.titleizeString(string); case 3: return JSPLib.utility.properCase(string); case 4: return JSPLib.utility.exceptCase(string); case 5: return JSPLib.utility.romanCase(string); case 0: default: return string; } } function FixupMetatag(value, metatag) { switch(metatag) { case '@': value.value = '@' + value.name; value.label = value.name; break; case "": value.value = value.name; value.label = value.name.replace(/_/g, ' '); break; default: metatag = metatag.replace(/:$/, ""); value.value = metatag + ':' + value.name; value.label = value.name.replace(/_/g, ' '); } } function SortSources(data) { var scaler; switch(IAC.postcount_scale[0]) { case 'logarithmic': scaler = ((num) => Math.log(num)); break; case 'square_root': scaler = ((num) => Math.sqrt(num)); break; case 'linear': default: scaler = ((num) => num); } data.sort((a, b) => { let mult_a = IAC[`${a.source}_source_weight`]; let mult_b = IAC[`${b.source}_source_weight`]; let weight_a = mult_a * scaler(a.post_count); let weight_b = mult_b * scaler(b.post_count); return weight_b - weight_a; }).forEach((entry, i) => { data[i] = entry; }); } function GroupSources(data) { let source_order = IAC.source_order; data.sort((a, b) => (source_order.indexOf(a.source) - source_order.indexOf(b.source))); } //Usage functions function KeepSourceData(type, metatag, data) { IAC.source_data[type] = IAC.source_data[type] || {}; data.forEach((val) => { let orig_key = val.name.replace(RegExp(`^${metatag}:?`), ""); let key = (val.antecedent ? val.antecedent + '\xff' + orig_key : orig_key); IAC.source_data[type][key] = val; }); } function GetChoiceOrder(type, query, word_mode, is_blank) { let queryterm = query.toLowerCase() + (type === 'metatag' && !query.endsWith('*') ? '*' : ""); var available_choices; if (!is_blank) { let regex = (word_mode ? WordRegex(queryterm, false) : GlobRegex(queryterm, false)); available_choices = IAC.choice_order[type].filter((name) => name.toLowerCase().match(regex)); } else { available_choices = IAC.choice_order[type]; } return available_choices.filter((tag) => (IAC.choice_data[type][tag].use_count > 0)).toSorted((a, b) => IAC.choice_data[type][b].use_count - IAC.choice_data[type][a].use_count); } function AddUserSelected(type, metatag, term, data, query_type, word_mode, key, is_blank) { IAC.shown_data = []; let order = IAC.choice_order[type]; let choice = IAC.choice_data[type]; if (!order || !choice) { return; } let user_order = GetChoiceOrder(type, term, word_mode, is_blank); let check_values = ['tag-edit', 'tag-query'].includes(query_type); let valid_values = (check_values ? JSPLib.utility.getObjectAttributes(data, 'name') : []); for (let i = user_order.length - 1; i >= 0; i--) { let checkterm = user_order[i]; if (query_type === 'tag' && choice[checkterm].category === METATAG_TAG_CATEGORY) { // Don't insert static tags on tag-only inputs continue; } if (check_values && choice[checkterm].category === METATAG_TAG_CATEGORY && !valid_values.includes(checkterm)) { // Don't insert static metatags where they're not valid: tag-query and tag-edit have different sets. continue; } //Splice out Danbooru data if it exists for (let j = 0; j < data.length; j++) { let compareterm = (data[j].antecedent ? data[j].antecedent + '\xff' + data[j].name : data[j].name); if (compareterm === checkterm) { data.splice(j, 1); //Should only be one of these at most break; } } let add_data = Object.assign({}, choice[user_order[i]], {term, key, type}); if (type === 'tag' && ['tag', 'tag-word'].includes(add_data.source)) { add_data.source = (word_mode ? 'tag-word' : 'tag'); } FixupMetatag(add_data, metatag); data.unshift(add_data); IAC.shown_data.push(user_order[i]); } data.splice(IAC.source_results_returned); } //For autocomplete select function InsertUserSelected(input, item) { const printer = JSPLib.debug.getFunctionPrint('InsertUserSelected'); if (!IAC.usage_enabled || !$(input).hasClass('iac-autocomplete')) { return; } var term, source_data; let type = item.type; if (item.category === BUR_TAG_CATEGORY) { return; } if ($(input).data('multiple') === false) { input.name = input.name.trim(); } if (item.antecedent) { term = item.antecedent + '\xff' + item.name; } else { term = item.name; } if (item.category === METATAG_TAG_CATEGORY || item.source === 'tag-abbreviation') { source_data = item; } else //Final failsafe if (!IAC.source_data[type] || !IAC.source_data[type][term]) { if (!IAC.choice_data[type] || !IAC.choice_data[type][term]) { printer.debuglog("Error: Bad data selector!", type, term, item); return; } source_data = IAC.choice_data[type][term]; } else { source_data = IAC.source_data[type][term]; } IAC.choice_order[type] = IAC.choice_order[type] || []; IAC.choice_data[type] = IAC.choice_data[type] || {}; IAC.choice_order[type].unshift(term); IAC.choice_order[type] = JSPLib.utility.arrayUnique(IAC.choice_order[type]); //So the use count doesn't get squashed by the new variable assignment let use_count = (IAC.choice_data[type][term] && IAC.choice_data[type][term].use_count) || 0; IAC.choice_data[type][term] = JSPLib.utility.dataCopy(source_data); ['key', 'term', 'label', 'value', 'type', 'original'].forEach((e) => {delete IAC.choice_data[type][term][e];}); IAC.choice_data[type][term].expires = JSPLib.utility.getExpires(GetUsageExpires()); IAC.choice_data[type][term].use_count = use_count + 1; if (IAC.usage_maximum > 0) { IAC.choice_data[type][term].use_count = Math.min(IAC.choice_data[type][term].use_count, IAC.usage_maximum); } IAC.shown_data.forEach((key) => { if (key !== term) { IAC.choice_data[type][key].use_count = IAC.choice_data[type][key].use_count || 0; IAC.choice_data[type][key].use_count *= IAC.usage_multiplier; } }); StoreUsageData('insert', term); } function InsertCompletion(input, item) { let completion = item.name; if (!$(input).hasClass('iac-autocomplete')) { Danbooru.Autocomplete.insert_completion(input, completion); return; } let query_type = GetQueryType(input); var before_caret_text = input.value.substring(0, input.selectionStart); var after_caret_text = input.value.substring(input.selectionStart); var regexp = new RegExp('(' + IAC.prefixes.join('|') + ')?\\S+$', 'g'); let $input = $(input); let start = 0, end = 0; if ($input.data('insert-autocomplete')) { let display_text = completion; let current_mode = AUTOCOMPLETE_MODE[IAC.ac_mode]; if (['tag', 'normal'].includes(current_mode)) { if (current_mode === 'normal') { display_text = display_text.replace(/_/g, ' '); display_text = CapitalizeAutocomplete(display_text); } before_caret_text = before_caret_text.replace(regexp, '$1') + '[[' + display_text + ']]'; start = end = before_caret_text.length; } else if (['pipe', 'custom'].includes(current_mode)) { let insert_text = "insert text"; if (current_mode === 'pipe') { display_text = display_text.replace(/_/g, ' '); display_text = CapitalizeAutocomplete(display_text); insert_text = ""; } before_caret_text = before_caret_text.replace(regexp, '$1') + `[[${display_text}|${insert_text}]]`; if (current_mode === 'pipe') { start = end = before_caret_text.length; } else { //Current mode == custom start = before_caret_text.length - 13; end = before_caret_text.length - 2; } } setTimeout(() => {DisableTextAreaAutocomplete($input);}, 1); } else { // Trim all whitespace (tabs, spaces) except for line returns before_caret_text = before_caret_text.replace(/^[ \t]+|[ \t]+$/gm, ""); after_caret_text = after_caret_text.replace(/^[ \t]+|[ \t]+$/gm, ""); before_caret_text = before_caret_text.substring(0, before_caret_text.search(/\S+$/)); var prefix = ""; var query = ParseQuery(input.value, input.selectionStart); if (item.source !== 'metatag') { prefix = query_type === 'tag-edit' || !CATEGORY_REGEX.test(query.prefix) ? query.prefix : query.operator; if (IAC.is_bur && IAC.BUR_source_enabled) { let line_text = before_caret_text.split('\n').at(-1); let words = line_text.split(/\s+/); if (words.length === 1 || words[0] !== 'update') { // Commands should not have a prefix, and only the update command should have tags with prefixes prefix = ""; } } } else { prefix = query.operator; } before_caret_text += prefix + completion + ' '; start = end = before_caret_text.length; } input.value = before_caret_text + after_caret_text; input.selectionStart = start; input.selectionEnd = end; $(input).trigger("input"); if (item.category === METATAG_TAG_CATEGORY && item.type === 'tag') { let mapping = (query_type === 'tag-edit' ? EDIT_METATAGS_MAP : QUERY_METATAGS_MAP); let option_metatags = (query_type === 'tag-query' ? ALL_OPTION_METATAGS : []); let normalized_metatag = item.name.slice(0, -1); input.selectionStart = input.selectionEnd = input.selectionStart - 1; if (item.source === 'metatag' && (normalized_metatag in mapping || option_metatags.includes(normalized_metatag))) { setTimeout(() => {$(input).autocomplete('search');}, 1); return; } } setTimeout(() => {$(input).autocomplete("instance").close();}, 1); } function StaticMetatagSource(term, metatag, query_type) { let lower_term = term.toLowerCase(); let full_term = `${metatag}:${lower_term}`; let data = SubmetatagData(query_type) .filter((item) => item.name.startsWith(full_term)) .map((item) => Object.assign({}, item, {term})) .sort((a, b) => a.name.localeCompare(b.name)) .slice(0, IAC.source_results_returned); AddUserSelected('metatag', "", full_term, data, query_type, false, null, false); return data; } function UserOptions(term, metatag, data) { if (USER_OPTION_METATAGS.includes(metatag.slice(0, -1))) { if (term.length === 0 || GetGlobMatches('none', term + '*')) { data.unshift({ name: 'none', level: 'system', }); } if (term.length === 0 || GetGlobMatches('any', term + '*')) { data.unshift({ name: 'any', level: 'system', }); } } return data; } function FavgroupOptions(term, metatag, data) { if (FAVGROUP_OPTION_METATAGS.includes(metatag.slice(0, -1))) { if (term.length === 0 || GetGlobMatches('none', term + '*')) { data.unshift({ name: 'none', post_count: 0, category: 'system', }); } if (term.length === 0 || GetGlobMatches('any', term + '*')) { data.unshift({ name: 'any', post_count: 0, category: 'system', }); } } return data; } function PoolOptions(term, metatag, data) { if (POOL_OPTION_METATAGS.includes(metatag.slice(0, -1))) { if (term.length === 0 || GetGlobMatches('collection', term + '*')) { data.unshift({ name: 'collection', post_count: 0, category: 'collection', }); } if (term.length === 0 || GetGlobMatches('series', term + '*')) { data.unshift({ name: 'series', post_count: 0, category: 'series', }); } if (term.length === 0 || GetGlobMatches('none', term + '*')) { data.unshift({ name: 'none', post_count: 0, category: 'system', }); } if (term.length === 0 || GetGlobMatches('any', term + '*')) { data.unshift({ name: 'any', post_count: 0, category: 'system', }); } } return data; } function SavedSearchOptions(term, metatag, data) { if (SAVED_SEARCH_OPTION_METATAGS.includes(metatag.slice(0, -1))) { if (term.length === 0 || GetGlobMatches('all', term + '*')) { data.unshift({ name: 'all', category: 'system', }); } } return data; } function TypeBlankResults(type, key, metatag, all_metatags, autocomplete) { if (all_metatags.includes(metatag.slice(0, -1))) { let input = autocomplete.element.get(0); let query_type = GetQueryType(input); return ProcessSourceData(type, key + '-', "", metatag, query_type, false, [], input, true); } return []; } //For autocomplete render function HighlightSelected($link, item) { if (IAC.source_highlight_enabled) { if (item.expires) { $($link).addClass('iac-user-choice'); } if (item.type === 'tag' || item.type === 'metatag') { $($link).addClass('iac-tag-highlight'); switch (item.source) { case 'tag': $($link).addClass('iac-tag-exact'); break; case 'tag-word': $($link).addClass('iac-tag-word'); break; case 'tag-abbreviation': $($link).addClass('iac-tag-abbreviation'); break; case 'tag-alias': $($link).addClass('iac-tag-alias'); break; case 'tag-autocorrect': $($link).addClass('iac-tag-autocorrect'); break; case 'tag-other-name': $($link).addClass('iac-tag-other-name'); break; case 'bur': $($link).addClass('iac-tag-bur'); break; case 'metatag': $($link).addClass('iac-tag-metatag'); //falls through default: //Do nothing } } if (IAC.highlight_used && IAC.current_tags.includes(item.name)) { $($link).addClass('iac-already-used'); } } if (IAC.highlight_words_enabled) { let term = item.term; if (item.type === 'tag' || item.type === 'metatag') { term += (item.source === 'metatag' && !item.term.endsWith('*') ? '*' : ""); let [tagname, tagclass] = (item.antecedent ? [item.antecedent, 'autocomplete-antecedent'] : [item.name, 'autocomplete-tag']); let highlight_html = (item.source === 'tag-word' ? HighlightWords(term, tagname) : HighlightGlobs(term, tagname, item.type)); if (highlight_html) { $link.find('.' + tagclass).html(highlight_html); } } else { let value = term; let highlight_html = HighlightGlobs(value, item.name); if (highlight_html) { $link.find('a').html(highlight_html); } } } if (item.source === 'metatag') { $('a', $link).addClass('tag-type-' + item.category); $('.post-count', $link).text('metatag'); } if (item.type === 'tag') { $($link).attr('data-autocomplete-type', item.source); } return $link; } function HighlightWords(search, name) { let regex = WordRegex(search, true); let capture_groups = WordRegex(search, true, true); let word_match = name.match(regex); if (!word_match) return null; let html_sections = word_match.slice(1).map((match, i) => { let label = match.replace(/_/g, " "); return (capture_groups[i] !== '(.*)' ? `${label}` : label); }); return html_sections.join(""); } function HighlightGlobs(search, name) { let regex = GlobRegex(search, true); let capture_groups = GlobRegex(search, true, true); let glob_match = name.match(regex); if (!glob_match) return null; let html_sections = glob_match.slice(1).map((match, i) => { let label = match.replace(/_/g, " "); return (capture_groups[i] !== '(.*)' ? `${label}` : label); }); return html_sections.join(""); } function CorrectUsageData() { const printer = JSPLib.debug.getFunctionPrint('CorrectUsageData'); let error_messages = ValidateUsageData(IAC); if (error_messages.length) { printer.debuglog("Corrections to usage data detected!"); error_messages.forEach((error) => {printer.debuglog(error);}); StoreUsageData('correction'); } else { printer.debuglog("Usage data is valid."); } } function PruneUsageData() { const printer = JSPLib.debug.getFunctionPrint('PruneUsageData'); let is_dirty = false; for (let type_key in IAC.choice_data) { let type_entry = IAC.choice_data[type_key]; for (let key in type_entry) { let entry = type_entry[key]; if (!JSPLib.utility.validateExpires(entry.expires, GetUsageExpires())) { printer.debuglog("Pruning choice data!", type_key, key); IAC.choice_order[type_key] = JSPLib.utility.arrayDifference(IAC.choice_order[type_key], [key]); delete type_entry[key]; is_dirty = true; } } } if (is_dirty) { StoreUsageData('prune'); } } function StoreUsageData(name, key = "", save = true) { if (save) { JSPLib.storage.setLocalData('iac-choice-info', {choice_order: IAC.choice_order, choice_data: IAC.choice_data}); } IAC.channel.postMessage({type: 'reload', name, key, choice_order: IAC.choice_order, choice_data: IAC.choice_data}); } ////Setup functions function RebindRender() { $(AUTOCOMPLETE_REBIND_SELECTORS).each((_, entry) => { let render_set = $(entry).data('iac-render'); let autocomplete = $(entry).data('uiAutocomplete'); if (!render_set && autocomplete) { autocomplete._renderItem = AutocompleteRenderItem; autocomplete._renderMenu = RenderMenuItem; $(entry).data('iac-render', true); } }); } function DelayInitializeAutocomplete(...args) { setTimeout(() => {InitializeAutocompleteIndexed(...args);}, JQUERY_DELAY); } function DelayInitializeTagAutocomplete(selector, type) { if (selector && type) { $(selector).attr('data-autocomplete', type); } clearTimeout(DelayInitializeTagAutocomplete.timer); DelayInitializeTagAutocomplete.timer = setTimeout(InitializeTagQueryAutocompleteIndexed, JQUERY_DELAY); } //Rebind callback functions function RebindRenderCheck() { JSPLib.utility.recheckInterval({ check: () => !JSPLib.utility.hasDOMDataKey(AUTOCOMPLETE_REBIND_SELECTORS, 'iac-render'), exec: RebindRender, interval: TIMER_POLL_INTERVAL, duration: JSPLib.utility.one_second * 5, }); } function RebindAutocomplete({selector, func, duration} = {}) { JSPLib.utility.recheckInterval({ check: () => JSPLib.utility.hasDOMDataKey(selector, 'uiAutocomplete'), exec: () => { $(selector).each((_, entry) => { if ($(entry).autocomplete('instance')) { $(entry).autocomplete('destroy').off('keydown.Autocomplete.tab'); } }); func(); }, interval: TIMER_POLL_INTERVAL, duration, }); } function RebindAnyAutocomplete(selector, keycode, multiple) { RebindAutocomplete({ selector, func () { InitializeAutocompleteIndexed(selector, keycode, {multiple}); }, }); } function RebindMultipleTag() { RebindAutocomplete({ selector: AUTOCOMPLETE_MULTITAG_SELECTORS, func () { InitializeTagQueryAutocompleteIndexed(); }, }); } function RebindSingleTag() { RebindAutocomplete({ selector: '[data-autocomplete=tag]', func () { let $fields = $('[data-autocomplete=tag]'); $fields.autocomplete({ minLength: 1, autoFocus: true, async source(request, respond) { let results = await IAC.tag_source(request.term, {autocomplete: this}); respond(results); }, select (_, ui) { InsertUserSelected(this, ui.item); }, }); $fields.addClass('iac-autocomplete'); setTimeout(() => { $fields.each((_, field) => { let autocomplete = $(field).data('uiAutocomplete'); autocomplete._renderItem = AutocompleteRenderItem; autocomplete._renderMenu = RenderMenuItem; }); }, JQUERY_DELAY); }, }); } function ReorderAutocompleteEvent($obj) { function RequeueEvent(str, event_array) { let position = event_array.findIndex((event) => event.namespace.startsWith(str)); let item = event_array.splice(position, 1); event_array.unshift(item[0]); } try { let private_data = JSPLib.utility.getPrivateData($obj[0]); let keydown_events = JSPLib.utility.getNestedAttribute(private_data, ['events', 'keydown']); RequeueEvent('autocomplete', keydown_events); //The tab event handler must go before the autocomplete handler RequeueEvent('Autocomplete.Tab', keydown_events); } catch (error) { JSPLib.debug.debugerror("Unable to reorder autocomplete events!", error); } } //Initialization functions function InitializeTagQueryAutocompleteIndexed(fields_selector = AUTOCOMPLETE_MULTITAG_SELECTORS, reorder_selector = '#post_tag_string') { let $fields_multiple = $(fields_selector); $fields_multiple.autocomplete({ select(event, ui) { if (event.key === "Enter") { event.stopImmediatePropagation(); } InsertCompletion(this, ui.item); InsertUserSelected(this, ui.item); return false; }, async source(request, respond) { var query_type = GetQueryType(this.element.get(0)); var query = ParseQuery(request.term, this.element.get(0).selectionStart); var metatag = query.metatag; var term = query.term; var prefix = query.prefix; var results = []; var metatag_type = null; var regex_map = (query_type === 'tag-edit' ? EDIT_METATAG_REGEXES : QUERY_METATAG_REGEXES); for (let key in regex_map) { let match = regex_map[key].exec(metatag); if (match) { metatag_type = key; break; } } switch (metatag_type) { case "static": results = IAC.static_metatag_source(term, metatag, query_type); break; case "user": results = await IAC.user_source(term, {prefix, autocomplete: this}); break; case "pool": results = await IAC.pool_source(term, {prefix, autocomplete: this}); break; case "favgroup": results = await IAC.favorite_group_source(term, {prefix, autocomplete: this}); break; case "search": results = await IAC.saved_search_source(term, {prefix, autocomplete: this}); break; case "tag": results = await IAC.tag_source(term, {autocomplete: this}); break; default: results = []; break; } respond(results); }, }); $fields_multiple.each((_, entry) => { let autocomplete = $(entry).data('uiAutocomplete'); autocomplete._renderItem = AutocompleteRenderItem; autocomplete._renderMenu = RenderMenuItem; }); $fields_multiple.addClass('iac-autocomplete'); if (reorder_selector) { let $tag_input_fields = $(reorder_selector); if ($tag_input_fields.length) { ReorderAutocompleteEvent($tag_input_fields); } } } function InitializeAutocompleteIndexed(selector, keycode, {multiple = false, wiki_link = false} = {}) { let type = SOURCE_KEY[keycode]; var $fields = $(selector); let autocomplete = AnySourceIndexed(keycode); $fields.autocomplete({ minLength: 1, delay: 100, async source(request, respond) { let parse = ParseQuery(request.term, this.element.get(0).selectionStart); if (parse.prefix || !parse.term) { respond([]); return; } let results = await autocomplete(parse.term, {autocomplete: this}); respond(results); }, select (event, ui) { InsertUserSelected(this, ui.item); if (multiple || wiki_link) { InsertCompletion(this, ui.item); if (wiki_link || event.key === 'Enter') { event.stopImmediatePropagation(); } return false; } ui.item.name = ui.item.name.trim(); return ui.item.name; }, }); let alink_func = (SOURCE_CONFIG[type].render ? SOURCE_CONFIG[type].render : ($domobj, item) => $domobj.text(item.name)); setTimeout(() => { $fields.each((_, field) => { let autocomplete = $(field).data('uiAutocomplete'); if (wiki_link) { autocomplete._renderItem = AutocompleteRenderItem; } else { autocomplete._renderItem = RenderListItem(alink_func); } autocomplete._renderMenu = RenderMenuItem; }); }, JQUERY_DELAY); if (!JSPLib.utility.isNamespaceBound(selector, 'keydown', 'Autocomplete.tab')) { $fields.on('keydown.Autocomplete.tab', null, 'tab', Danbooru.Autocomplete.on_tab); } $fields.data('autocomplete', type); $fields.data('multiple', multiple || wiki_link); $fields.addClass('iac-autocomplete'); } function InitializeTextAreaAutocomplete(selector = 'textarea:not([data-autocomplete]), input[type=text]:not([data-autocomplete])') { IAC.ac_source = JSPLib.storage.getLocalData('iac-ac-source', {default_val: 0}); IAC.ac_mode = JSPLib.storage.getLocalData('iac-ac-mode', {default_val: 0}); IAC.ac_caps = JSPLib.storage.getLocalData('iac-ac-caps', {default_val: 0}); $(selector).filter(':not(.iac-autocomplete)').on(JSPLib.program_keydown, null, 'alt+a', (event) => { let $input = $(event.currentTarget); let type = AUTOCOMPLETE_SOURCE[IAC.ac_source]; if (!$input.data('insert-autocomplete')) { EnableTextAreaAutocomplete($input, type); } else { DisableTextAreaAutocomplete($input, type); } }).data('insert-autocomplete', false); $(selector).filter(':not(.iac-autocomplete)').on(JSPLib.program_keydown, null, 'alt+1 alt+2 alt+3', (event) => { if (event.originalEvent.key === '1') { IAC.ac_source = (IAC.ac_source + 1) % AUTOCOMPLETE_SOURCE.length; JSPLib.notice.notice(RenderAutocompleteNotice('source', AUTOCOMPLETE_SOURCE, IAC.ac_source)); JSPLib.storage.setLocalData('iac-ac-source', IAC.ac_source); } else if (event.originalEvent.key === '2') { IAC.ac_mode = (IAC.ac_mode + 1) % AUTOCOMPLETE_MODE.length; JSPLib.notice.notice(RenderAutocompleteNotice('mode', AUTOCOMPLETE_MODE, IAC.ac_mode)); JSPLib.storage.setLocalData('iac-ac-mode', IAC.ac_mode); } else if (event.originalEvent.key === '3') { IAC.ac_caps = (IAC.ac_caps + 1) % AUTOCOMPLETE_CAPITALIZATION.length; JSPLib.notice.notice(RenderAutocompleteNotice('capitalization', AUTOCOMPLETE_CAPITALIZATION, IAC.ac_caps)); JSPLib.storage.setLocalData('iac-ac-caps', IAC.ac_caps); } IAC.channel.postMessage({type: 'text_autocomplete', source: IAC.ac_source, mode: IAC.ac_mode, caps: IAC.ac_caps}); }).addClass('iac-autocomplete'); } function EnableTextAreaAutocomplete($input, type) { if ($input.closest('.autocomplete-mentions').length > 0) { $input.autocomplete('destroy').off('keydown.Autocomplete.tab'); } let input_selector = JSPLib.utility.getHTMLTree($input[0]); let type_shortcut = PROGRAM_DATA_KEY[type]; InitializeAutocompleteIndexed(input_selector, type_shortcut, {wiki_link: true}); $input.data('insert-autocomplete', true); $input.data('autocomplete', 'tag'); JSPLib.notice.notice(JSPLib.utility.sprintf(AUTOCOMPLETE_MESSAGE, AUTOCOMPLETE_SOURCE[IAC.ac_source], AUTOCOMPLETE_MODE[IAC.ac_mode], AUTOCOMPLETE_CAPITALIZATION[IAC.ac_caps])); } function DisableTextAreaAutocomplete($input) { $input.autocomplete('destroy').off('keydown.Autocomplete.tab'); $input.data('insert-autocomplete', false); $input.data('autocomplete', ""); JSPLib.notice.notice("Autocomplete turned off!"); if ($input.closest('.autocomplete-mentions').length > 0) { Danbooru.Autocomplete.initialize_mention_autocomplete($input); } } //Main auxiliary functions async function NetworkSource(type, key, term, {metatag = null, query_type = null, word_mode = null, element = null, process = true} = {}) { const printer = JSPLib.debug.getFunctionPrint('NetworkSource'); const CONFIG = SOURCE_CONFIG[type]; if (CONFIG.invalid?.(term)) { printer.debuglog("Invalid search term for", query_type ?? type, ':', term); return []; } printer.debuglog("Querying", type, ':', term); let url_addons = $.extend({limit: IAC.source_results_returned}, CONFIG.data(term, query_type)); let network_data = await JSPLib.danbooru.submitRequest(CONFIG.url, url_addons); if (!network_data || !Array.isArray(network_data)) { return []; } var data = network_data.map((item) => CONFIG.map(item, term)); var expiration_time = CONFIG.expiration(data); var save_data = JSPLib.utility.dataCopy(data); JSPLib.storage.saveData(key, {value: save_data, expires: JSPLib.utility.getExpires(expiration_time)}); if (process) { return ProcessSourceData(type, key, term, metatag, query_type, word_mode, data, element, false); } } function AnySourceIndexed(keycode) { var type = SOURCE_KEY[keycode]; return async function (term, {prefix, autocomplete} = {}) { if ((!SOURCE_CONFIG[type].spaces_allowed || JSPLib.utility.isString(prefix)) && term.match(/\S\s/)) { return []; } term = term.trim(); if (term === "") { switch (type) { case 'user': return TypeBlankResults('user', 'us', prefix, USER_OPTION_METATAGS, autocomplete); case 'favgroup': return TypeBlankResults('favgroup', 'fg', prefix, FAVGROUP_OPTION_METATAGS, autocomplete); case 'pool': return TypeBlankResults('pool', 'pl', prefix, POOL_OPTION_METATAGS, autocomplete); case 'search': return TypeBlankResults('search', 'ss', prefix, SAVED_SEARCH_OPTION_METATAGS, autocomplete); default: return []; } } var word_mode = false; if (type === 'tag' && !term.startsWith('/') && !term.endsWith('*')) { word_mode = term.length > 1 && !( IAC.word_start_matches || (SOURCE_CONFIG[type] === SOURCE_CONFIG.tag2) || (IAC.alternate_tag_wildcards && Boolean(term.match(/\*/))) ); term += (!word_mode && !term.endsWith('*') ? '*' : ""); } else { term += (term.endsWith('*') ? "" : '*'); term = (SOURCE_CONFIG[type].search_start ? "" : "*") + term; } var key = (keycode + '-' + term).toLowerCase(); var metatag = (JSPLib.utility.isString(prefix) ? prefix : ""); var query_type = GetQueryType(autocomplete.element.get(0)); var final_data = null; if (!IAC.network_only_mode) { var max_expiration = MaximumExpirationTime(type); var cached = await JSPLib.storage.checkLocalDB(key, max_expiration); if (ValidateCached(cached, type, term, word_mode)) { RecheckSourceData(type, key, term, cached); final_data = ProcessSourceData(type, key, term, metatag, query_type, word_mode, cached.value, autocomplete.element.get(0), false); } } if (!final_data) { final_data = NetworkSource(type, key, term, {metatag, query_type, word_mode, element: autocomplete.element.get(0)}); } return final_data; }; } function RecheckSourceData(type, key, term, data) { const printer = JSPLib.debug.getFunctionPrint('RecheckSourceData'); if (IAC.recheck_data_interval > 0) { let recheck_time = data.expires - GetRecheckExpires(); if (!JSPLib.utility.validateExpires(recheck_time)) { printer.debuglog("Rechecking", type, ':', term); NetworkSource(type, key, term, {process: false}); } } } function ProcessSourceData(type, key, term, metatag, query_type, word_mode, data, element, is_blank) { if (query_type === 'tag-query') { switch (type) { case 'user': data = UserOptions(term, metatag, data); break; case 'favgroup': data = FavgroupOptions(term, metatag, data); break; case 'pool': data = PoolOptions(term, metatag, data); break; case 'search': data = SavedSearchOptions(term, metatag, data); // falls through default: // do nothing } } data.forEach((val) => { FixupMetatag(val, metatag); Object.assign(val, {term, key, type}); }); KeepSourceData(type, metatag, data); if (type === 'tag') { if (IAC.alternate_sorting_enabled) { SortSources(data); } if (IAC.metatag_source_enabled) { if (query_type !== 'tag') { let regex = new RegExp('^' + JSPLib.utility.regexpEscape(term).replace(/\\\*/g, '.*')); let filter_data = MetatagData(query_type).filter((data) => data.name.match(regex)); let metatag_term = term + (term.endsWith('*') ? "" : '*'); let add_data = filter_data.map((item) => Object.assign({term: metatag_term}, item)); data.unshift(...add_data); } } if (IAC.source_grouping_enabled) { GroupSources(data); } } if (IAC.usage_enabled) { AddUserSelected(type, metatag, term, data, query_type, word_mode, key, is_blank); } if (IAC.is_bur && IAC.BUR_source_enabled && element.id === 'bulk_update_request_script') { let line_text = element.value.substring(0, element.selectionStart).split('\n').at(-1).trim(); let words = line_text.split(/\s+/); if (words.length === 1) { // Show only BUR commands for the first word of each line. data = GetConstantMatches(BUR_DATA, term); } else if ((words.length > 2 && words.at(-1) === '->') || (words.length > BUR_LIMITS[words[0]])) { // For the 3rd word and beyond, don't show the autocomplte when the right arrow is detected. return []; } else if (words[0] === 'category' && words.length > 3) { // Show tag categories for the 4th word of the category command. data = GetConstantMatches(CATEGORY_DATA, term); } } //Doing this here to avoid processing it on each list item IAC.highlight_used = query_type === 'tag-edit'; if (IAC.highlight_used) { let adjusted_tag_string = RemoveTerm(element.value, element.selectionStart); IAC.current_tags = adjusted_tag_string.split(/\s+/); } return data; } //Main execution functions function SetupAutocompleteInitializations() { switch (IAC.controller) { case 'wiki-pages': case 'wiki-page-versions': RebindAnyAutocomplete('[data-autocomplete=wiki-page]', 'wp'); break; case 'artists': case 'artist-versions': case 'artist-urls': RebindAnyAutocomplete('[data-autocomplete=artist]', 'ar'); break; case 'pools': case 'pool-versions': RebindAnyAutocomplete('[data-autocomplete=pool]', 'pl'); break; case 'favorite-groups': RebindAnyAutocomplete('[data-autocomplete=favorite-group]', 'fg'); break; case 'posts': if (IAC.action === 'index') { RebindAnyAutocomplete('[data-autocomplete=saved-search-label]', 'ss', true); } break; case 'saved-searches': if (IAC.action === 'index') { DelayInitializeTagAutocomplete('#search_query_ilike', 'tag-query'); RebindAnyAutocomplete('[data-autocomplete=saved-search-label]', 'ss'); } else if (IAC.action === 'edit') { DelayInitializeTagAutocomplete('#saved_search_query', 'tag-query'); RebindAnyAutocomplete('[data-autocomplete=saved-search-label]', 'ss', true); } break; case 'forum-posts': if (IAC.action === 'search') { DelayInitializeAutocomplete('#search_topic_title_matches', 'ft'); } break; case 'comments': DelayInitializeTagAutocomplete(); break; case 'uploads': if (IAC.action === 'index') { DelayInitializeTagAutocomplete('#search_post_tags_match', 'tag-query'); } break; case 'bulk-update-requests': if (IAC.is_bur) { DelayInitializeTagAutocomplete('#bulk_update_request_script', 'tag-edit'); } break; case 'related-tags': DelayInitializeTagAutocomplete('#search_query', 'tag-query'); //falls through default: //do nothing } if ($(AUTOCOMPLETE_REBIND_SELECTORS).length) { RebindRenderCheck(); } if ($('[data-autocomplete=tag]').length) { RebindSingleTag(); } if ($(AUTOCOMPLETE_MULTITAG_SELECTORS).length) { RebindMultipleTag(); } if ($(AUTOCOMPLETE_USER_SELECTORS).length) { RebindAnyAutocomplete(AUTOCOMPLETE_USER_SELECTORS, 'us'); } if (IAC.text_input_autocomplete_enabled) { InitializeTextAreaAutocomplete(); } } function CleanupTasks() { PruneUsageData(); JSPLib.storage.pruneProgramCache(PROGRAM_DATA_REGEX, PRUNE_EXPIRES); } //Cache functions function UpdateLocalData(key, data) { switch (key) { case 'iac-choice-info': IAC.choice_order = data.choice_order; IAC.choice_data = data.choice_data; StoreUsageData('save', "", false); //falls through default: //Do nothing } } //Settings functions function BroadcastIAC(event) { const printer = JSPLib.debug.getFunctionPrint('BroadcastIAC'); printer.debuglog(`(${event.data.type}): ${event.data.name} ${event.data.key}`); switch (event.data.type) { case 'text_autocomplete': IAC.ac_source = event.data.source; IAC.ac_mode = event.data.mode; IAC.ac_caps = event.data.caps; break; case 'reload': IAC.choice_order = event.data.choice_order; IAC.choice_data = event.data.choice_data; //falls through default: //do nothing } } function RemoteSettingsCallback() { SetTagAutocompleteSource(); } function SetTagAutocompleteSource() { if (IAC.alternate_tag_source) { SOURCE_CONFIG.tag = SOURCE_CONFIG.tag2; } else { SOURCE_CONFIG.tag = SOURCE_CONFIG.tag1; } } function GetUsageExpires() { return IAC.usage_expires * JSPLib.utility.one_day; } function GetRecheckExpires() { return IAC.recheck_data_interval * JSPLib.utility.one_day; } function InitializeProgramValues(override = false) { if (InitializeProgramValues.initialized) return; const printer = JSPLib.debug.getFunctionPrint('InitializeProgramValues'); if (!JSPLib.storage.use_indexed_db) { printer.debugwarn("No Indexed DB! Exiting..."); return false; } if (!override && document.querySelector(AUTOCOMPLETE_ALL_SELECTORS) === null) { printer.debugwarn("No autocomplete inputs! Exiting..."); return false; } Object.assign(IAC, { user_id: Danbooru.CurrentUser.data('id'), choice_info: JSPLib.storage.getLocalData('iac-choice-info', {default_val: {}}), is_bur: (IAC.controller === 'bulk-update-requests') && ['edit', 'new'].includes(IAC.action), prefixes: JSON.parse(JSPLib.utility.getMeta('autocomplete-tag-prefixes')), }, PROGRAM_RESET_KEYS); Object.assign(IAC, { categories: IAC.prefixes.filter((key) => (!['-', '~'].includes(key))).map((key) => (key.slice(0, -1))), }); if (JSPLib.utility.isHash(IAC.choice_info)) { IAC.choice_order = IAC.choice_info.choice_order; IAC.choice_data = IAC.choice_info.choice_data; } Object.assign(IAC, { tag_source: AnySourceIndexed('ac'), pool_source: AnySourceIndexed('pl'), user_source: AnySourceIndexed('us'), favorite_group_source: AnySourceIndexed('fg'), saved_search_source: AnySourceIndexed('ss'), static_metatag_source: StaticMetatagSource, }); SetTagAutocompleteSource(); CorrectUsageData(); InitializeProgramValues.initialized = true; return true; } function RenderSettingsMenu() { $('#indexed-autocomplete').append(JSPLib.menu.renderMenuFramework(MENU_CONFIG)); $('#iac-general-settings-message').append(JSPLib.menu.renderExpandable("Text autocomplete details", TEXT_AUTOCOMPLETE_DETAILS)); $('#iac-general-settings').append(JSPLib.menu.renderDomainSelectors()); $('#iac-general-settings').append(JSPLib.menu.renderCheckbox('text_input_autocomplete_enabled')); $('#iac-source-settings').append(JSPLib.menu.renderCheckbox('BUR_source_enabled')); $('#iac-source-settings').append(JSPLib.menu.renderCheckbox('metatag_source_enabled')); $('#iac-usage-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", USAGE_SETTINGS_DETAILS)); $('#iac-usage-settings').append(JSPLib.menu.renderCheckbox('usage_enabled')); $('#iac-usage-settings').append(JSPLib.menu.renderTextinput('usage_multiplier')); $('#iac-usage-settings').append(JSPLib.menu.renderTextinput('usage_maximum')); $('#iac-usage-settings').append(JSPLib.menu.renderTextinput('usage_expires')); $('#iac-display-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", DISPLAY_SETTINGS_DETAILS)); $('#iac-display-settings').append(JSPLib.menu.renderTextinput('source_results_returned', 5)); $('#iac-display-settings').append(JSPLib.menu.renderCheckbox('source_highlight_enabled')); $('#iac-display-settings').append(JSPLib.menu.renderCheckbox('highlight_words_enabled')); $('#iac-display-settings').append(JSPLib.menu.renderCheckbox('source_grouping_enabled')); $('#iac-display-settings').append(JSPLib.menu.renderSortlist('source_order')); $('#iac-sort-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", SORT_SETTINGS_DETAILS)); $('#iac-sort-settings').append(JSPLib.menu.renderCheckbox('alternate_sorting_enabled')); $('#iac-sort-settings').append(JSPLib.menu.renderInputSelectors('postcount_scale', 'radio')); $('#iac-sort-settings').append(JSPLib.menu.renderTextinput('exact_source_weight', 5)); $('#iac-sort-settings').append(JSPLib.menu.renderTextinput('prefix_source_weight', 5)); $('#iac-sort-settings').append(JSPLib.menu.renderTextinput('alias_source_weight', 5)); $('#iac-sort-settings').append(JSPLib.menu.renderTextinput('correct_source_weight', 5)); $('#iac-network-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", NETWORK_SETTINGS_DETAILS)); $('#iac-network-settings').append(JSPLib.menu.renderTextinput('recheck_data_interval', 5)); $('#iac-network-settings').append(JSPLib.menu.renderCheckbox('alternate_tag_source')); $('#iac-network-settings').append(JSPLib.menu.renderCheckbox('alternate_tag_wildcards')); $('#iac-network-settings').append(JSPLib.menu.renderCheckbox('word_start_matches')); $('#iac-network-settings').append(JSPLib.menu.renderCheckbox('network_only_mode')); $('#iac-controls').append(JSPLib.menu.renderCacheControls()); $('#iac-cache-controls-message').append(JSPLib.menu.renderExpandable("Cache Data details", CACHE_DATA_DETAILS)); $('#iac-cache-controls').append(JSPLib.menu.renderLinkclick('cache_info', true)); $('#iac-cache-controls').append(JSPLib.menu.renderCacheInfoTable()); $('#iac-cache-controls').append(JSPLib.menu.renderLinkclick('purge_cache', true)); $('#iac-controls').append(JSPLib.menu.renderCacheEditor(true)); $('#iac-cache-editor-message').append(JSPLib.menu.renderExpandable("Program Data details", PROGRAM_DATA_DETAILS)); $('#iac-cache-editor-controls').append(JSPLib.menu.renderKeyselect('data_source', true)); $('#iac-cache-editor-controls').append(JSPLib.menu.renderDataSourceSections()); $('#iac-section-indexed-db').append(JSPLib.menu.renderKeyselect('data_type', true)); $('#iac-section-indexed-db').append(JSPLib.menu.renderKeyselect('related_tag_type', true)); $('#iac-section-local-storage').append(JSPLib.menu.renderCheckbox('raw_data', true)); $('#iac-cache-editor-controls').append(JSPLib.menu.renderTextinput('data_name', 20, true)); $('.iac-options[data-setting=related_tag_type]').hide(); JSPLib.menu.engageUI(true, true); JSPLib.menu.saveUserSettingsClick(RemoteSettingsCallback); JSPLib.menu.resetUserSettingsClick(LOCALSTORAGE_KEYS, RemoteSettingsCallback); JSPLib.menu.cacheInfoClick(); JSPLib.menu.purgeCacheClick(); JSPLib.menu.expandableClick(); JSPLib.menu.dataSourceChange(); JSPLib.menu.rawDataChange(); JSPLib.menu.getCacheClick(ValidateProgramData); JSPLib.menu.saveCacheClick(ValidateProgramData, ValidateEntry, UpdateLocalData); JSPLib.menu.deleteCacheClick(); JSPLib.menu.listCacheClick(); JSPLib.menu.refreshCacheClick(); JSPLib.menu.cacheAutocomplete(); } //Main program function Main() { const preload = { run_on_settings: true, default_data: DEFAULT_VALUES, initialize_func: InitializeProgramValues, broadcast_func: BroadcastIAC, render_menu_func: RenderSettingsMenu, program_css: PROGRAM_CSS, light_css: LIGHT_MODE_CSS, dark_css: DARK_MODE_CSS, menu_css: SETTINGS_MENU_CSS, }; if (!JSPLib.menu.preloadScript(IAC, preload)) return; SetupAutocompleteInitializations(); JSPLib.statistics.addPageStatistics(); JSPLib.load.noncriticalTasks(CleanupTasks); } /****Initialization****/ //Variables for JSPLib JSPLib.program_name = PROGRAM_NAME; JSPLib.program_shortcut = PROGRAM_SHORTCUT; JSPLib.program_data = IAC; //Variables for debug.js JSPLib.debug.mode = false; JSPLib.debug.level = JSPLib.debug.INFO; //Variables for menu.js JSPLib.menu.program_reset_data = PROGRAM_RESET_KEYS; JSPLib.menu.program_data_regex = PROGRAM_DATA_REGEX; JSPLib.menu.settings_callback = RemoteSettingsCallback; JSPLib.menu.reset_callback = RemoteSettingsCallback; JSPLib.menu.settings_config = SETTINGS_CONFIG; JSPLib.menu.control_config = CONTROL_CONFIG; //Variables for storage.js JSPLib.storage.indexedDBValidator = ValidateEntry; //Export JSPLib JSPLib.load.exportData(); JSPLib.load.exportFuncs({always_list: [InitializeAutocompleteIndexed, InitializeTagQueryAutocompleteIndexed, InitializeTextAreaAutocomplete, InitializeProgramValues]}); /****Execution start****/ JSPLib.load.programInitialize(Main, {required_variables: PROGRAM_LOAD_REQUIRED_VARIABLES, required_selectors: PROGRAM_LOAD_REQUIRED_SELECTORS});