// ==UserScript==
// @name IndexedAutocomplete
// @namespace https://github.com/BrokenEagle/JavaScripts
// @version 29.21
// @description Uses Indexed DB for autocomplete, plus caching of other data.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match *://*.donmai.us/*
// @exclude /^https?://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @grant none
// @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://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/20220515/lib/module.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/debug.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/utility.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/validate.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/storage.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/concurrency.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/statistics.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/network.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/danbooru.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/load.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20240223-menu/lib/menu.js
// ==/UserScript==
/* global JSPLib $ Danbooru validate */
/****Global variables****/
//Library constants
const LIBRARY_MENU_CSS = `
/* LIBRARY FIXES */
#userscript-settings-menu .jsplib-settings-buttons input {
color: var(--button-primary-text-color);
}
#page #userscript-settings-menu .jsplib-settings-buttons .jsplib-commit:hover {
background-color: var(--green-5);
}
#page #userscript-settings-menu .jsplib-settings-buttons .jsplib-resetall:hover {
background-color: var(--red-5);
}
#userscript-settings-menu .jsplib-settings-buttons .jsplib-commit:hover,
#userscript-settings-menu .jsplib-settings-buttons .jsplib-resetall:hover {
filter: brightness(1.25);
}`;
//Exterior script variables
const DANBOORU_TOPIC_ID = '14701';
//Variables for load.js
const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery', 'window.Danbooru', 'Danbooru.Autocomplete', 'Danbooru.RelatedTag', 'Danbooru.CurrentUser'];
const PROGRAM_LOAD_REQUIRED_SELECTORS = ['#top', '#page'];
//Program name constants
const PROGRAM_SHORTCUT = 'iac';
const PROGRAM_KEYDOWN = 'keydown.iac';
const PROGRAM_NAME = 'IndexedAutocomplete';
//Program data constants
const PROGRAM_DATA_REGEX = /^(af|ref|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.validate.isBoolean,
hint: "Uncheck to turn off usage mechanism."
},
alternate_sorting_enabled: {
reset: false,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Adds metatags to autocomplete results on all post tag search inputs."
},
BUR_source_enabled: {
reset: true,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Adds highlights and stylings to the HTML classes set by the program."
},
highlight_words_enabled: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Underlines word matches on word match results."
},
source_grouping_enabled: {
reset: true,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Uses the /tags
controller instead of the normal autocomplete source."
},
alternate_tag_wildcards: {
reset: false,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Always adds a wildcard to the end, which forces the old behavior of searching from the beginning only."
},
network_only_mode: {
reset: false,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Enables autocomplete in non-autocomplete text fields (Alt+A to enable/disable), inserting a wiki link upon completion."
},
forum_quick_search_enabled: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Adds a quick search bar where applicable using forum topic titles."
},
comment_quick_search_enabled: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Adds a quick search bar where applicable using post search queries."
},
};
//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 = Object.assign({
query_UID: {},
}, 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;
}
.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 > span,
iac-search > span {
flex-basis: 100%;
}
.iac-user-choice .autocomplete-item {
box-shadow: 0px 2px 0px #000;
padding-bottom: 1px;
line-height: 150%;
display: inline-block;
}
.iac-tag-alias a {
font-style: italic;
}
.iac-tag-highlight {
margin-top: -5px;
margin-bottom: 5px;
}
.iac-tag-highlight > div:before {
content: "●";
padding-right: 4px;
font-weight: bold;
font-size: 150%;
}
.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;
}
/** DARK/LIGHT Color Setup **/
body[data-current-user-theme=light] .iac-already-used {
background-color: #FFFFAA;
}
body[data-current-user-theme=light] .iac-tag-metatag > div:before,
body[data-current-user-theme=light] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=light] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=light] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #000;
}
body[data-current-user-theme=light] .iac-highlight-match {
filter: brightness(0.75);
}
body[data-current-user-theme=dark] .iac-already-used {
background-color: #666622;
}
body[data-current-user-theme=dark] .iac-tag-metatag > div:before,
body[data-current-user-theme=dark] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=dark] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=dark] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #FFF;
}
body[data-current-user-theme=dark] .iac-highlight-match {
filter: brightness(1.25);
}
@media (prefers-color-scheme: light) {
body[data-current-user-theme=auto] .iac-already-used {
background-color: #FFFFAA;
}
body[data-current-user-theme=auto] .iac-tag-metatag > div:before,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #000;
}
body[data-current-user-theme=auto] .iac-highlight-match {
filter: brightness(0.75);
}
}
@media (prefers-color-scheme: dark) {
body[data-current-user-theme=auto] .iac-already-used {
background-color: #666622;
}
body[data-current-user-theme=auto] .iac-tag-metatag > div:before,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=auto] .iac-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #FFF;
}
body[data-current-user-theme=auto] .iac-highlight-match {
filter: brightness(1.25);
}
}
`;
const FORUM_CSS = `
body[data-current-user-theme=light] .ui-menu-item .forum-topic-category-0 {
color: blue;
}
body[data-current-user-theme=light] .ui-menu-item .forum-topic-category-1 {
color: green;
}
body[data-current-user-theme=light] .ui-menu-item .forum-topic-category-2 {
color: red;
}
body[data-current-user-theme=dark] .ui-menu-item .forum-topic-category-0 {
color: var(--blue-3);
}
body[data-current-user-theme=dark] .ui-menu-item .forum-topic-category-1 {
color: var(--green-3);
}
body[data-current-user-theme=dark] .ui-menu-item .forum-topic-category-2 {
color: var(--red-3);
}
@media (prefers-color-scheme: light) {
.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;
}
}
@media (prefers-color-scheme: dark) {
.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 FORUM_TOPIC_SEARCH = `
`;
const POST_COMMENT_SEARCH = `
`;
const TEXT_AUTOCOMPLETE_DETAILS = `
- Source (Alt+1):
tag
- Will search tags.
wiki
- Will search wikis.
- Mode (Alt+2):
tag
- Spaces appear as underscores.
normal
- Spaces appear as spaces, uses the entire value.
pipe
- Spaces appear as spaces, places a pipe "|" at the end.
- This will remove a final parentheses value.
- E.g. "pokemon_(game)" will appear as "pokemon".
custom
- Default "insert text" in place for custom text by user.
- Capitalization (Alt+3):
lowercase
- All lowercase letters.
uppercase
- All uppercase letters.
titlecase
- Only first letter capitalized.
propercase
- First letter of every word capitalized.
exceptcase
- Propercase except for "a", "an", "of", "the", "is".
romancase
- Exceptcase plus capitalize all letters in Roman numerals.
`;
const USAGE_SETTINGS_DETAILS = `
Equations
- Hit:usage_count = Min( usage_count + 1 , usage_maximum )
- Miss:usage_count = usage_count * usage_multiplier
`;
const DISPLAY_SETTINGS_DETAILS = `
- Source highlight enabled: The following are the CSS classes and default styling.
.iac-user-choice
- bold text
.iac-tag-exact
- light grey dot
.iac-tag-word
- dark grey dot
.iac-tag-abbreviation
- pink dot
.iac-tag-alias
- gold dot, italic text
.iac-tag-other-name
- orange dot
.iac-tag-autocorrect
- cyan dot
- Source highlight enabled: Class and default style:
.iac-word-match
- underline
- Source grouping enabled:
- When not enabled, the default is to order using the post count and a weighting scheme.
sort_value = post_count x weight_value
- The different weights are: (Exact: 1.0), (Prefix: 0.8), (Alias: 0.2), (Correct: 0.1).
- Source order:
- Exact: Matches exactly letter for letter.
- Prefix: Matches the first letter of each word.
- Alias: Same as exact, but it checks aliases.
- Correct: Tags off by 1-3 letters, i.e. mispellings.
`;
const SORT_SETTINGS_DETAILS = `
- Alternate sorting must be enabled to use the alternate scales/weights.
- These settings won't affect anything if source grouping is enabled.
Equations
- Linear:tag_weight = source_weight x post_count
- Square root:tag_weight = source_weight x Sqrt( post_count )
- Logarithmic:tag_weight = source_weight x Log( post_count )
`;
const NETWORK_SETTINGS_DETAILS = `
- Alternate tag source: No tag correct or tag prefix matches.
- Alternate tag wildcards: This uses the
/tags
endpoint instead of the usual /autocomplete
one when wildcards are used, though that shouldn't change the results being returned.
[Different wildcard bedhavior]
- No wildcards - A wildcard always gets appended to the end of the string.
- Danbooru wildcards - The wildcards get used as they are input, and no wildcard is appended at the end.
- Alternate wildcards - A wildcard always gets appended to the end of the string.
- Network only mode:
- Can be used to correct cache data that has been changed on the server.
- Warning! As this negates the benefits of using local cached data, it should only be used sparingly.
`;
const CACHE_DATA_DETAILS = `
- Autocomplete data: Data from every combination of keys in the text input.
- tags (ac)
- pools (pl)
- users (us)
- favorite groups (fg)
- saved searches (ss)
- wiki pages (wp)
- artists (ar)
- forum topics (ft)
`;
const PROGRAM_DATA_DETAILS = `
All timestamps are in milliseconds since the epoch (Epoch converter).
- General data
- prune-expires: When the program will next check for cache data that has expired.
- user-settings: All configurable settings.
- Text autocomplete data
- ac-source: Numerical value designating the current source.
- ac-mode: Numerical value designating the current mode.
- ac-caps: Numerical value designating the current capitalization.
- Usage data
- choice-info: Comprised of choice order and choice data
- choice_order: The search terms per source ordered by last use.
- The order in this list only affects things when the usage counts of two terms are equal.
- choice_data: The search terms per source with the autocomplete data.
- The use_count affects how terms get sorted against each other.
- The expiration affects when data gets pruned, and gets renewed each time a term is selected.
`;
const AUTOCOMPLETE_MESSAGE = `
Autocomplete turned on!
Source: | %s |
Mode: | %s |
Capitalization: | %s |
`;
//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 ALL_METATAGS = JSPLib.utility.multiConcat([
'user', 'approver', 'commenter', 'comm', 'noter', 'noteupdater', 'artcomm', 'commentaryupdater',
'flagger', 'appealer', 'upvote', 'downvote', 'fav', 'ordfav', 'favgroup', 'ordfavgroup', 'pool',
'ordpool', 'note', 'comment', 'commentary', 'id', 'rating', 'locked', 'source', 'status', 'filetype',
'disapproved', 'parent', 'child', 'search', 'embedded', 'md5', 'width', 'height', 'mpixels', 'ratio',
'score', 'favcount', 'filesize', 'date', 'age', 'order', 'limit', 'tagcount', 'pixiv_id', 'pixiv',
'unaliased', 'exif', 'duration', 'random', 'is', 'has',
'user', 'approver', 'commenter', 'comm', 'noter', 'noteupdater', 'artcomm', 'commentaryupdater',
'flagger', 'appealer', 'upvote', 'downvote', 'fav', 'ordfav', 'favgroup', 'ordfavgroup', 'pool',
'ordpool', 'note', 'comment', 'commentary', 'id', 'rating', 'source', 'status', 'filetype',
'disapproved', 'parent', 'child', 'search', 'embedded', 'md5', 'width', 'height', 'mpixels', 'ratio',
'score', 'upvotes', 'downvotes', 'favcount', 'filesize', 'date', 'age', 'order', 'limit', 'tagcount', 'pixiv_id', 'pixiv',
'unaliased', 'exif', 'duration', 'random', 'is', 'has', 'ai',
], COUNT_METATAGS, COUNT_METATAG_SYNONYMS, CATEGORY_COUNT_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 STATIC_METATAGS = {
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'], POST_STATUSES),
child: JSPLib.utility.concat(['any', 'none'], POST_STATUSES),
parent: JSPLib.utility.concat(['any', 'none'], POST_STATUSES),
rating: POST_RATINGS,
embedded: ['true', 'false'],
filetype: FILE_TYPES,
commentary: ['true', 'false', 'translated', 'untranslated'],
disapproved: ['breaks_rules', 'poor_quality', 'disinterest'],
order: ORDER_METATAGS,
};
const TYPE_TAGS = ['ch', 'co', 'gen', 'char', 'copy', 'art', 'meta', 'general', 'character', 'copyright', 'artist'];
//Regex constants
const TERM_REGEX = RegExp('([-~]*)(?:(' + JSPLib.utility.concat(ALL_METATAGS, TYPE_TAGS).join('|') + '):)?(\\S*)$', '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'];
const BUR_DATA = BUR_KEYWORDS.map((tag) => ({
type: 'tag',
label: tag,
name: tag,
post_count: 'BUR',
source: 'bur',
category: BUR_TAG_CATEGORY,
key: null,
}));
//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
const CALLBACK_INTERVAL = 1000; //Interval for fixup callback functions
//Data inclusion lists
const ALL_CATEGORIES = [0, 1, 3, 4, 5];
const ALL_TOPICS = [0, 1, 2];
const ALL_POOLS = ['collection', 'series'];
const ALL_USERS = ['Member', 'Gold', 'Platinum', 'Builder', 'Moderator', 'Admin'];
//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 = [
'#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',
data: (term) => (
{
search: {
type: 'tag_query',
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')),
fixupexpiration: false,
searchstart: true,
spacesallowed: false
},
tag2: {
url: 'tags',
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')),
fixupexpiration: false,
searchstart: true,
spacesallowed: 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')),
fixupexpiration: false,
searchstart: false,
spacesallowed: true,
render: ($domobj, item) => {
let html = `
${item.label}
${item.post_count}
`;
return $(html);
},
},
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'),
fixupexpiration: false,
searchstart: true,
spacesallowed: false,
render: ($domobj, item) => {
let html = `
`;
return $(html);
},
},
favgroup: {
url: 'favorite_groups',
data: (term) => ({
search: {
name_matches: term,
creator_id: IAC.userid,
},
only: 'name,post_ids'
}),
map: (favgroup) => ({
name: favgroup.name,
post_count: favgroup.post_ids.length,
}),
expiration: () => MinimumExpirationTime('favgroup'),
fixupexpiration: false,
searchstart: false,
spacesallowed: true,
render: ($domobj, item) => {
let html = `
${item.label}
${item.post_count}
`;
return $(html);
},
},
search: {
url: 'autocomplete',
data: (term) => ({
search: {
type: 'saved_search_label',
query: term,
}
}),
map: (label) => ({
name: label.value,
}),
expiration: () => MinimumExpirationTime('search'),
fixupexpiration: false,
searchstart: true,
spacesallowed: false,
render: ($domobj, item) => {
let html = `
`;
return $(html);
},
},
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: () => MinimumExpirationTime('wikipage'),
fixupexpiration: true,
searchstart: true,
spacesallowed: true,
render: ($domobj, item) => {
let count = (item.no_tag ? 'No tag' : item.post_count);
let html = `
`;
return $(html);
},
},
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: () => MinimumExpirationTime('artist'),
fixupexpiration: true,
searchstart: true,
spacesallowed: false,
render: ($domobj, item) => {
let count = (item.no_tag ? 'No tag' : item.post_count);
let html = `
`;
return $(html);
},
},
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'),
fixupexpiration: false,
searchstart: false,
spacesallowed: true,
render: ($domobj, item) => {
let html = `
${item.label}
${item.response_count}
`;
return $(html);
},
}
};
//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,
},
search: {
name: JSPLib.validate.stringonly_constraints,
},
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****/
//Library functions
JSPLib.menu.preloadScript = function (self, program_value, render_menu_func, {run_on_settings = false, default_data = {}, reset_data = {}, initialize_func = null, broadcast_func = null, menu_css = null} = {}) {
program_value.user_settings = this.loadUserSettings();
for (let key in program_value.user_settings) {
Object.defineProperty(program_value, key, {get() {return program_value.user_settings[key];}});
}
if (this._isSettingMenu()) {
this.initializeSettingsMenu(render_menu_func, menu_css);
if (!run_on_settings) return false;
}
if (!this.isScriptEnabled()) {
self.debug('logLevel', "Script is disabled on", window.location.hostname, JSPLib.debug.INFO);
return false;
}
Object.assign(program_value, {
controller: document.body.dataset.controller,
action: document.body.dataset.action,
}, JSPLib.utility.dataCopy(default_data), JSPLib.utility.dataCopy(reset_data));
if (typeof broadcast_func == 'function') {
program_value.channel = JSPLib.utility.createBroadcastChannel(this.program_name, broadcast_func);
}
if (typeof initialize_func == 'function') {
return initialize_func();
}
return true;
};
JSPLib.utility.getHTMLTree = function (domnode) {
var tree = [];
for (let checknode = domnode; checknode !== null; checknode = checknode.parentElement) {
let nodename = checknode.tagName.toLowerCase();
let id = (checknode.id !== "" ? "#" : "") + checknode.id;
let classlist = [...checknode.classList].map((entry) => (!entry.includes(':') ? '.' + entry : "")).join("");
let index = "";
if (checknode.parentElement !== null) {
let similar_elements = [...checknode.parentElement.children].filter((entry) => entry.tagName === checknode.tagName);
let similar_position = similar_elements.indexOf(checknode) + 1;
index = ":nth-of-type(" + similar_position + ")";
}
tree.push(nodename + id + classlist + index);
}
return tree.reverse().join(" > ");
};
JSPLib.debug.addModuleLogs('menu', ['preloadScript']);
//Validate functions
function ValidateEntry(key, entry) {
if (!JSPLib.validate.validateIsHash(key, entry)) {
return false;
}
if (key.match(/^(?:ac|pl|us|fg|ss|ar|wp|ft)-/)) {
return ValidateAutocompleteEntry(key, entry);
}
this.debug('log', "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.validate.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.validate.isHash(choice_order) || !JSPLib.validate.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.validate.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.validate.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 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 GetPrefix(str) {
if (!(str in GetPrefix.prefixhash)) {
GetPrefix.prefixhash[str] = str.split('_').map((part) => (part.replace(/[()]/g, "")[0])).join("");
}
return GetPrefix.prefixhash[str];
}
GetPrefix.prefixhash = {};
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;
}
function GetIsBur() {
return (IAC.controller === 'bulk-update-requests') && ['edit', 'new'].includes(IAC.action);
}
function GetHasQuickSearchBar() {
return ['forum-topics', 'forum-posts', 'comments'].includes(IAC.controller);
}
const MapMetatag = (type, metatag, value) => ({
type,
antecedent: null,
label: metatag + ':' + value,
value: metatag + ':' + value,
name: metatag + ':' + value,
post_count: METATAG_TAG_CATEGORY,
source: 'metatag',
category: METATAG_TAG_CATEGORY
});
function MetatagData() {
if (!MetatagData.data) {
MetatagData.data = ALL_METATAGS
.filter((tag) => (tag[0] !== '-'))
.map((tag) => (MapMetatag('tag', tag, "")));
}
return MetatagData.data;
}
function SubmetatagData() {
if (!SubmetatagData.data) {
SubmetatagData.data = [];
for (let metatag in STATIC_METATAGS) {
for (let i = 0; i < STATIC_METATAGS[metatag].length; i++) {
let submetatag = STATIC_METATAGS[metatag][i];
SubmetatagData.data.push(MapMetatag('metatag', metatag, submetatag));
}
}
}
return SubmetatagData.data;
}
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_old(list, item);
}
if (SOURCE_CONFIG[item.type].render) {
return RenderListItem(SOURCE_CONFIG[item.type].render)(list, item);
}
let tag_info = "";
if (item.antecedent) {
tag_info = `${item.label}`;
let antecedent = item.antecedent.replace(/_/g, " ");
tag_info = '→' + tag_info;
tag_info = `${antecedent}` + tag_info;
} else {
tag_info = `${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 = `
${tag_info}
${post_text}`;
let data_items = ["type", "antecedent", "value", "category", "post_count"].map((attr) => `data-autocomplete-${attr.replace(/_/g, "-")}="${item[attr]}"`);
let $list_item = $(`
${line_item}
`);
$list_item.data("item.autocomplete", item);
$list_item.find('a').on('click.iac', (event) => {event.preventDefault();});
return $list_item.appendTo(list);
}
function RenderListItem(alink_func) {
return function (list, item) {
let $link = alink_func($(''), item);
let $container = $('').append($link);
HighlightSelected($container, list, item);
return $('').data('item.autocomplete', item).append($container).appendTo(list);
};
}
function RenderMenuItem() {
return function (ul, items) {
let event_UID = items.UID || IAC.query_UID[items.key];
if (Number.isInteger(event_UID)) {
JSPLib.debug.debugTime('renderitem-' + event_UID);
}
items.forEach((item) => {
this._renderItemData(ul, item);
});
if (Number.isInteger(event_UID)) {
JSPLib.debug.debugTimeEnd('renderitem-' + event_UID);
JSPLib.debug.debugTimeEnd('autocomplete-' + event_UID);
}
};
}
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)));
}
function FixExpirationCallback(key, value, tagname, type) {
this.debug('log', "Fixing expiration:", tagname);
JSPLib.danbooru.submitRequest('tags', {search: {name: tagname}}).then((data) => {
if (!data.length) {
return;
}
let expiration_time = ExpirationTime(type, data[0].post_count);
JSPLib.storage.saveData(key, {value, expires: JSPLib.utility.getExpires(expiration_time)});
});
}
//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) {
let queryterm = query.toLowerCase() + (type === 'metatag' && !query.endsWith('*') ? '*' : "");
let regex = (word_mode ? WordRegex(queryterm, false) : GlobRegex(queryterm, false));
let available_choices = IAC.choice_order[type].filter((name) => name.toLowerCase().match(regex));
let sortable_choices = available_choices.filter((tag) => (IAC.choice_data[type][tag].use_count > 0));
sortable_choices.sort((a, b) => IAC.choice_data[type][b].use_count - IAC.choice_data[type][a].use_count);
return JSPLib.utility.arrayUnique(sortable_choices.concat(available_choices));
}
function AddUserSelected(type, metatag, term, data, query_type, word_mode, key) {
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);
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) {
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(data, input, selected) {
if (!IAC.usage_enabled || !$(input).hasClass('iac-autocomplete')) {
return;
}
var type, item, term, source_data;
//Being hamstrung by Danbooru's select function of the multi-source tag complete
if (typeof selected === 'string') {
let autocomplete = $(input).autocomplete('instance');
let list_container = autocomplete.menu.element[0];
let $links = $('.ui-state-active', list_container).parent();
if ($links.length === 0) {
$links = $('.ui-menu-item:first-of-type', list_container);
}
item = $links.data('item.autocomplete');
if (!item) {
this.debug('log', "Error: No autocomplete data found!", $links, item);
return;
}
type = item.type;
if (!type) {
let autocomplete_type = $(input).data('autocomplete');
if (autocomplete_type === 'tag-query' || autocomplete_type === 'tag-edit') {
let match = selected.match(TERM_REGEX);
type = (match[2] && match[3].length ? match[2] : 'tag');
} else {
type = autocomplete_type.replace(/-/g, "");
}
}
} else {
item = selected;
type = SOURCE_KEY[data];
}
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) {
if (item.type === 'tag') {
input.selectionStart = input.selectionEnd = input.selectionStart - 1;
setTimeout(() => {$(input).autocomplete('search');}, 100);
}
source_data = item;
} else if (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]) {
this.debug('log', "Error: Bad data selector!", type, term, selected, data, 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'].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, completion) {
if (!$(input).hasClass('iac-autocomplete')) {
Danbooru.Autocomplete.insert_completion_old(input, completion);
return;
}
// Trim all whitespace (tabs, spaces) except for line returns
var before_caret_text = input.value.substring(0, input.selectionStart).replace(/^[ \t]+|[ \t]+$/gm, "");
var after_caret_text = input.value.substring(input.selectionStart).replace(/^[ \t]+|[ \t]+$/gm, "");
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);}, 100);
} else {
var query = ParseQuery(input.value, input.selectionStart);
var select = ParseQuery(completion, completion.length);
before_caret_text = before_caret_text.substring(0, before_caret_text.search(/\S+$/));
var prefix = (query.metatag !== "tag" ? (query.prefix || select.prefix) : query.operator);
var name = (query.metatag === "tag" ? completion : select.term);
before_caret_text += prefix + name + ' ';
start = end = before_caret_text.length;
}
input.value = before_caret_text + after_caret_text;
input.selectionStart = start;
input.selectionEnd = end;
$(input).trigger("input");
$(() => $(input).autocomplete("instance").close());
}
function StaticMetatagSource(term, metatag) {
let lower_term = term.toLowerCase();
let full_term = `${metatag}:${lower_term}`;
let data = SubmetatagData()
.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, false, null);
return data;
}
//For autocomplete render
function HighlightSelected($link, list, 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() {
let error_messages = ValidateUsageData(IAC);
if (error_messages.length) {
this.debug('log', "Corrections to usage data detected!");
error_messages.forEach((error) => {this.debug('log', error);});
StoreUsageData('correction');
} else {
this.debug('log', "Usage data is valid.");
}
}
function 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())) {
this.debug('log', "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.setStorageData('iac-choice-info', {choice_order: IAC.choice_order, choice_data: IAC.choice_data}, localStorage);
}
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((i, entry) => {
let render_set = $(entry).data('iac-render');
let autocomplete = $(entry).data('uiAutocomplete');
if (!render_set && autocomplete) {
autocomplete._renderItem = Danbooru.Autocomplete.render_item;
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(DanbooruIntializeTagAutocomplete, JQUERY_DELAY);
}
//Rebind callback functions
function RebindRenderCheck() {
JSPLib.utility.recheckTimer({
check: () => !JSPLib.utility.hasDOMDataKey(AUTOCOMPLETE_REBIND_SELECTORS, 'iac-render'),
exec: RebindRender,
}, TIMER_POLL_INTERVAL, JSPLib.utility.one_second * 5);
}
function RebindAnyAutocomplete(selector, keycode, multiple) {
JSPLib.utility.recheckTimer({
check: () => JSPLib.utility.hasDOMDataKey(selector, 'uiAutocomplete'),
exec: () => {
$(selector).autocomplete('destroy').off('keydown.Autocomplete.tab');
InitializeAutocompleteIndexed(selector, keycode, multiple);
}
}, TIMER_POLL_INTERVAL);
}
function RebindMultipleTag() {
JSPLib.utility.recheckTimer({
check: () => JSPLib.utility.hasDOMDataKey(AUTOCOMPLETE_MULTITAG_SELECTORS, 'uiAutocomplete'),
exec: () => {
$(AUTOCOMPLETE_MULTITAG_SELECTORS).autocomplete('destroy').off('keydown.Autocomplete.tab');
DanbooruIntializeTagAutocomplete();
}
}, TIMER_POLL_INTERVAL);
}
function RebindSingleTag() {
JSPLib.utility.recheckTimer({
check: () => JSPLib.utility.hasDOMDataKey('[data-autocomplete=tag]', 'uiAutocomplete'),
exec: () => {
let autocomplete = AnySourceIndexed('ac', true);
let $fields = $('[data-autocomplete=tag]');
$fields.autocomplete('destroy').off('keydown.Autocomplete.tab');
$fields.autocomplete({
minLength: 1,
autoFocus: true,
async source(request, respond) {
let results = await autocomplete.call(this, request.term);
respond(results);
},
select (event, ui) {
InsertUserSelected('ac', this, ui.item);
},
});
$fields.addClass('iac-autocomplete');
setTimeout(() => {
$fields.each((i, field) => {
let autocomplete = $(field).data('uiAutocomplete');
autocomplete._renderItem = Danbooru.Autocomplete.render_item;
autocomplete._renderMenu = RenderMenuItem();
});
}, JQUERY_DELAY);
}
}, TIMER_POLL_INTERVAL);
}
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 DanbooruIntializeTagAutocomplete() {
var $fields_multiple = $(AUTOCOMPLETE_MULTITAG_SELECTORS);
$fields_multiple.autocomplete({
select(event, ui) {
if (event.key === "Enter") {
event.stopImmediatePropagation();
}
Danbooru.Autocomplete.insert_completion(this, ui.item.name);
return false;
},
async source(req, resp) {
var query = ParseQuery(req.term, this.element.get(0).selectionStart);
var metatag = query.metatag;
var term = query.term;
var prefix = query.prefix;
var results = [];
switch (metatag) {
case "order":
case "status":
case "rating":
case "locked":
case "child":
case "parent":
case "filetype":
case "disapproved":
case "embedded":
case "commentary":
case "is":
case "has":
results = Danbooru.Autocomplete.static_metatag_source(term, metatag);
break;
case "user":
case "approver":
case "commenter":
case "comm":
case "noter":
case "noteupdater":
case "commentaryupdater":
case "artcomm":
case "fav":
case "ordfav":
case "appealer":
case "flagger":
case "upvote":
case "downvote":
results = await Danbooru.Autocomplete.user_source(term, prefix);
break;
case "pool":
case "ordpool":
results = await Danbooru.Autocomplete.pool_source(term, prefix);
break;
case "favgroup":
case "ordfavgroup":
results = await Danbooru.Autocomplete.favorite_group_source(term, prefix, Danbooru.CurrentUser.data("id"));
break;
case "search":
results = await Danbooru.Autocomplete.saved_search_source(term, prefix);
break;
case "tag":
results = await Danbooru.Autocomplete.tag_source(term);
break;
default:
results = [];
break;
}
resp(results);
},
});
$fields_multiple.each((i, entry) => {
let autocomplete = $(entry).data('uiAutocomplete');
autocomplete._renderItem = Danbooru.Autocomplete.render_item;
autocomplete._renderMenu = RenderMenuItem();
});
let $tag_input_fields = $("#upload_tag_string, #post_tag_string");
if ($tag_input_fields.length) {
ReorderAutocompleteEvent($tag_input_fields);
}
$fields_multiple.addClass('iac-autocomplete');
}
function InitializeAutocompleteIndexed(selector, keycode, multiple = false, wiki = false) {
let type = SOURCE_KEY[keycode];
var $fields = $(selector);
let autocomplete = AnySourceIndexed(keycode, true);
$fields.autocomplete({
minLength: 1,
delay: 100,
async source(request, respond) {
var term;
if (multiple || wiki) {
term = ParseQuery(request.term, this.element.get(0).selectionStart).term;
if (!term) {
respond([]);
return;
}
} else {
term = request.term;
}
let results = await autocomplete.call(this, term);
respond(results);
},
select (event, ui) {
InsertUserSelected(keycode, this, ui.item);
if (wiki) {
InsertCompletion(this, ui.item.name);
event.stopImmediatePropagation();
return false;
} if (multiple) {
if (event.key === 'Enter') {
event.stopImmediatePropagation();
}
Danbooru.Autocomplete.insert_completion_old(this, ui.item.name);
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((i, field) => {
let autocomplete = $(field).data('uiAutocomplete');
if (wiki) {
autocomplete._renderItem = Danbooru.Autocomplete.render_item;
} 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);
$fields.addClass('iac-autocomplete');
}
function InitializeTextAreaAutocomplete() {
IAC.ac_source = JSPLib.storage.getStorageData('iac-ac-source', localStorage, 0);
IAC.ac_mode = JSPLib.storage.getStorageData('iac-ac-mode', localStorage, 0);
IAC.ac_caps = JSPLib.storage.getStorageData('iac-ac-caps', localStorage, 0);
$('textarea:not([data-autocomplete]), input[type=text]:not([data-autocomplete])').on(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);
$('textarea:not([data-autocomplete]), input[type=text]:not([data-autocomplete])').on(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.setStorageData('iac-ac-source', IAC.ac_source, localStorage);
} 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.setStorageData('iac-ac-mode', IAC.ac_mode, localStorage);
} 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.setStorageData('iac-ac-caps', IAC.ac_caps, localStorage);
}
IAC.channel.postMessage({type: 'text_autocomplete', source: IAC.ac_source, mode: IAC.ac_mode, caps: IAC.ac_caps});
});
}
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, false, true);
$input.data('insert-autocomplete', true);
$input.data('autocomplete', 'tag-edit');
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, query_type, word_mode, process = true) {
this.debug('log', "Querying", type, ':', term);
const CONFIG = SOURCE_CONFIG[type];
let url_addons = $.extend({limit: IAC.source_results_returned}, CONFIG.data(term));
let data = await JSPLib.danbooru.submitRequest(CONFIG.url, url_addons);
if (!data || !Array.isArray(data)) {
return [];
}
var d = data.map((item) => CONFIG.map(item, term));
var expiration_time = CONFIG.expiration(d);
var save_data = JSPLib.utility.dataCopy(d);
JSPLib.storage.saveData(key, {value: save_data, expires: JSPLib.utility.getExpires(expiration_time)});
if (CONFIG.fixupexpiration && d.length) {
setTimeout(() => {FixExpirationCallback(key, save_data, save_data[0].value, type);}, CALLBACK_INTERVAL);
}
if (process) {
return ProcessSourceData(type, metatag, term, d, query_type, key, word_mode);
}
}
function AnySourceIndexed(keycode, has_context = false) {
var type = SOURCE_KEY[keycode];
return async function (term, prefix) {
if ((!SOURCE_CONFIG[type].spacesallowed || JSPLib.validate.isString(prefix)) && term.match(/\S\s/)) {
return [];
}
term = term.trim();
if (term === "") {
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].searchstart ? "" : "*") + term;
}
var key = (keycode + '-' + term).toLowerCase();
let UID = IAC.query_UID[key] = JSPLib.utility.getUniqueID();
JSPLib.debug.debugTime('autocomplete-' + UID);
JSPLib.debug.debugTime('source-' + UID);
var use_metatag = (JSPLib.validate.isString(prefix) ? prefix : "");
var query_type = (has_context ? $(this.element).data('autocomplete') : null);
var final_data = null;
if (!IAC.network_only_mode) {
var max_expiration = MaximumExpirationTime(type);
var cached = await JSPLib.storage.checkLocalDB(key, ValidateEntry, max_expiration);
if (ValidateCached(cached, type, term, word_mode)) {
RecheckSourceData(type, key, term, cached, word_mode);
final_data = ProcessSourceData(type, use_metatag, term, cached.value, query_type, key, word_mode);
}
}
if (!final_data) {
final_data = NetworkSource(type, key, term, use_metatag, query_type, word_mode);
}
JSPLib.debug.debugTimeEnd('source-' + UID);
Object.assign(final_data, {UID, term, key});
return final_data;
};
}
function RecheckSourceData(type, key, term, data, word_mode) {
if (IAC.recheck_data_interval > 0) {
let recheck_time = data.expires - GetRecheckExpires();
if (!JSPLib.utility.validateExpires(recheck_time)) {
this.debug('log', "Rechecking", type, ':', term);
NetworkSource(type, key, term, null, null, word_mode, false);
}
}
}
function ProcessSourceData(type, metatag, term, data, query_type, key, word_mode = false) {
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().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);
}
if (IAC.is_bur && IAC.BUR_source_enabled) {
let add_data = BUR_DATA.filter((data) => (term.length === 2 || GetGlobMatches(data.name, term))).map((data) => Object.assign({term}, data));
data.unshift(...add_data);
data.splice(IAC.source_results_returned);
}
//Doing this here to avoid processing it on each list item
IAC.highlight_used = (document.activeElement.tagName === 'TEXTAREA' && ['post_tag_string', 'upload_tag_string'].includes(document.activeElement.id));
if (IAC.highlight_used) {
let adjusted_tag_string = RemoveTerm(document.activeElement.value, document.activeElement.selectionStart);
IAC.current_tags = adjusted_tag_string.split(/\s+/);
}
return data;
}
//Main execution functions
function InstallQuickSearchBars() {
if (IAC.forum_quick_search_enabled && (IAC.controller === 'forum-topics' || IAC.controller === 'forum-posts')) {
JSPLib.utility.setCSSStyle(FORUM_CSS, 'forum');
$('#subnav-menu .search_body_matches').closest('li').after(FORUM_TOPIC_SEARCH);
}
if (IAC.comment_quick_search_enabled && IAC.controller === 'comments') {
$('#subnav-menu .search_body_matches').closest('li').after(POST_COMMENT_SEARCH);
}
}
function SetupAutocompleteBindings() {
Danbooru.Autocomplete.tag_source = AnySourceIndexed('ac');
Danbooru.Autocomplete.pool_source = AnySourceIndexed('pl');
Danbooru.Autocomplete.user_source = AnySourceIndexed('us');
Danbooru.Autocomplete.favorite_group_source = AnySourceIndexed('fg');
Danbooru.Autocomplete.saved_search_source = AnySourceIndexed('ss');
Danbooru.Autocomplete.static_metatag_source = StaticMetatagSource;
Danbooru.Autocomplete.insert_completion_old = Danbooru.Autocomplete.insert_completion;
Danbooru.Autocomplete.insert_completion = JSPLib.utility.hijackFunction(InsertCompletion, InsertUserSelected);
Danbooru.Autocomplete.render_item_old = Danbooru.Autocomplete.render_item;
Danbooru.Autocomplete.render_item = JSPLib.utility.hijackFunction(AutocompleteRenderItem, HighlightSelected);
Danbooru.Autocomplete.initialize_tag_autocomplete_old = Danbooru.Autocomplete.initialize_tag_autocomplete;
Danbooru.Autocomplete.initialize_tag_autocomplete = DanbooruIntializeTagAutocomplete;
}
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-topics':
case 'forum-posts':
DelayInitializeAutocomplete('#quick_search_title_matches', 'ft');
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.pruneEntries(PROGRAM_SHORTCUT, 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) {
this.debug('log', `(${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() {
if (!JSPLib.storage.use_indexed_db) {
this.debug('warn', "No Indexed DB! Exiting...");
return false;
}
if (document.querySelector(AUTOCOMPLETE_ALL_SELECTORS) === null && !GetHasQuickSearchBar()) {
this.debug('warn', "No autocomplete inputs! Exiting...");
return false;
}
Object.assign(IAC, {
userid: Danbooru.CurrentUser.data('id'),
});
Object.assign(IAC, {
choice_info: JSPLib.storage.getStorageData('iac-choice-info', localStorage, {}),
is_bur: GetIsBur(),
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.validate.isHash(IAC.choice_info)) {
IAC.choice_order = IAC.choice_info.choice_order;
IAC.choice_data = IAC.choice_info.choice_data;
}
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-general-settings').append(JSPLib.menu.renderCheckbox('forum_quick_search_enabled'));
$('#iac-general-settings').append(JSPLib.menu.renderCheckbox('comment_quick_search_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() {
this.debug('log', "Initialize start:", JSPLib.utility.getProgramTime());
const preload = {
run_on_settings: true,
default_data: DEFAULT_VALUES,
initialize_func: InitializeProgramValues,
broadcast_func: BroadcastIAC,
menu_css: SETTINGS_MENU_CSS + '\n' + LIBRARY_MENU_CSS,
};
if (!JSPLib.menu.preloadScript(IAC, RenderSettingsMenu, preload)) return;
InstallQuickSearchBars();
JSPLib.load.setProgramGetter(IAC, 'RTC', 'RecentTagsCalc');
CorrectUsageData();
SetupAutocompleteBindings();
SetupAutocompleteInitializations();
SetTagAutocompleteSource();
JSPLib.utility.setCSSStyle(PROGRAM_CSS, 'program');
JSPLib.statistics.addPageStatistics(PROGRAM_NAME);
JSPLib.load.noncriticalTasks(CleanupTasks);
}
/****Function decoration****/
[
Main, BroadcastIAC, NetworkSource, PruneUsageData, CorrectUsageData, InsertUserSelected,
FixExpirationCallback, ValidateEntry,
RecheckSourceData, InitializeProgramValues,
] = JSPLib.debug.addFunctionLogs([
Main, BroadcastIAC, NetworkSource, PruneUsageData, CorrectUsageData, InsertUserSelected,
FixExpirationCallback, ValidateEntry,
RecheckSourceData, InitializeProgramValues,
]);
[
ValidateUsageData, RenderSettingsMenu,
] = JSPLib.debug.addFunctionTimers([
//Sync
ValidateUsageData, RenderSettingsMenu,
]);
/****Initialization****/
//Variables for debug.js
JSPLib.debug.debug_console = false;
JSPLib.debug.level = JSPLib.debug.INFO;
JSPLib.debug.program_shortcut = PROGRAM_SHORTCUT;
//Variables for menu.js
JSPLib.menu.program_shortcut = PROGRAM_SHORTCUT;
JSPLib.menu.program_name = PROGRAM_NAME;
JSPLib.menu.program_data = IAC;
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;
//Export JSPLib
JSPLib.load.exportData(PROGRAM_NAME, IAC, {other_data: TERM_REGEX, datalist: ['cached_data']});
JSPLib.load.exportFuncs(PROGRAM_NAME, {debuglist: [DanbooruIntializeTagAutocomplete], alwayslist: [InitializeAutocompleteIndexed]});
/****Execution start****/
JSPLib.load.programInitialize(Main, {program_name: PROGRAM_NAME, required_variables: PROGRAM_LOAD_REQUIRED_VARIABLES, required_selectors: PROGRAM_LOAD_REQUIRED_SELECTORS});