// ==UserScript== // @name TranslatorAssist // @namespace https://github.com/BrokenEagle/JavaScripts // @version 6.4 // @description Provide information and tools for help with translations. // @source https://danbooru.donmai.us/users/23799 // @author BrokenEagle // @match https://*.donmai.us/posts/* // @match https://*.donmai.us/settings // @exclude /^https?://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/ // @grant GM.xmlHttpRequest // @run-at document-idle // @downloadURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/TranslatorAssist.user.js // @updateURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/TranslatorAssist.user.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/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 // @connect validator.nu // ==/UserScript== // eslint-disable-next-line no-redeclare /* global $ JSPLib Danbooru GM */ /* eslint-disable dot-notation */ /****Global variables****/ //Library constants ////NONE //Exterior script variables const DANBOORU_TOPIC_ID = '20687'; const GITHUB_WIKI_PAGE = 'https://github.com/BrokenEagle/JavaScripts/wiki/TranslatorAssist'; //Variables for load.js const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery', 'window.Danbooru', 'Danbooru.CurrentUser', 'Danbooru.Note']; const PROGRAM_LOAD_OPTIONAL_SELECTORS = ['#c-posts #a-show .image-container', '#c-users #a-edit']; //Program name constants const PROGRAM_SHORTCUT = 'ta'; const PROGRAM_CLICK = 'click.ta'; const PROGRAM_KEYDOWN = 'keydown.ta'; const PROGRAM_KEYUP = 'keyup.ta'; const PROGRAM_NAME = 'TranslatorAssist'; //Main program variable const TA = {}; const DEFAULT_VALUES = { initialized: false, close_notice_shown: false, last_noter_queried: false, side_menu_open: false, noter_detected: false, missed_poll: false, last_id: 0, save_data: null, shadow_grid: {}, $load_dialog: {}, }; //Available setting values const HTML_STYLE_TAGS = ['div', 'span']; const HTML_ONLY_TAGS = ['b', 'i', 'u', 's', 'tn', 'center', 'p', 'small', 'big', 'code']; const HTML_TAGS = JSPLib.utility.concat(HTML_STYLE_TAGS, HTML_ONLY_TAGS); const HTML_STYLES = ['color', 'font-size', 'font-family', 'font-weight', 'font-style', 'font-variant', 'text-align', 'text-decoration', 'line-height', 'letter-spacing', 'margin', 'padding', 'white-space', 'background-color', 'transform']; const OUTER_RUBY_STYLES = ['color', 'font-size', 'font-family', 'font-weight', 'font-style', 'font-variant', 'text-decoration', 'line-height', 'letter-spacing', 'padding', 'white-space', 'background-color']; const INNER_RUBY_STYLES = ['color', 'font-size', 'font-family', 'font-weight', 'font-style', 'font-variant', 'text-decoration', 'letter-spacing']; const RUBY_STYLES = OUTER_RUBY_STYLES; const EMBEDDED_STYLES = ['border-radius', 'rotate', 'background-color', 'justify-content', 'align-items']; //Main settings const SETTINGS_CONFIG = { close_notice_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Show a notice when closing the side menu." }, check_last_noted_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Show a notice when navigating to a post if the post has been noted within a cutoff period." }, last_noted_cutoff: { reset: 15, parse: parseInt, validate: (data) => (Number.isInteger(data) && data > 0), hint: "Number of minutes used as a cutoff when determining whether to show the last noted notice (greater than 0)." }, query_last_noter_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Query for the last noter when opening the side menu." }, last_noter_cache_time: { reset: 5, parse: parseInt, validate: (data) => (Number.isInteger(data) && data >= 0), hint: "Number of minutes to cache the query last noter data (greater than 0; setting to zero disables caching)." }, filter_last_noter_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Filter out self edits when checking for the last noter." }, new_noter_check_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Poll for new noters when the side menu is open." }, new_noter_check_interval: { reset: 5, parse: parseInt, validate: (data) => (Number.isInteger(data) && data > 0), hint: "How often to check for new noters (# of minutes)." }, available_html_tags: { allitems: HTML_TAGS, reset: HTML_TAGS, display: "Available HTML tags", validate: (data) => (JSPLib.menu.validateCheckboxRadio(data, 'checkbox', HTML_TAGS) && data.length > 0), hint: "Select the list of available HTML tags to be shown. Must have at least one." }, available_css_styles: { allitems: HTML_STYLES, reset: HTML_STYLES, display: "Available CSS styles", validate: (data) => (JSPLib.menu.validateCheckboxRadio(data, 'checkbox', HTML_STYLES) && data.length > 0), hint: "Select the list of available HTML styles to be shown. Must have at least one." }, text_shadow_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Uncheck to removed text shadow section." }, ruby_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Uncheck to removed ruby section." }, available_ruby_styles: { allitems: RUBY_STYLES, reset: RUBY_STYLES, validate: (data) => (JSPLib.menu.validateCheckboxRadio(data, 'checkbox', RUBY_STYLES) && data.length > 0), hint: "Select the list of available ruby styles to be shown. Must have at least one." }, embedded_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Uncheck to removed embedded tab." }, available_embedded_styles: { allitems: EMBEDDED_STYLES, reset: EMBEDDED_STYLES, validate: (data) => (JSPLib.menu.validateCheckboxRadio(data, 'checkbox', EMBEDDED_STYLES) && data.length > 0), hint: "Select the list of available embedded styles to be shown. Must have at least one." }, controls_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Uncheck to removed controls tab." }, codes_enabled: { reset: true, validate: JSPLib.validate.isBoolean, hint: "Uncheck to removed codes tab." }, }; const MENU_CONFIG = { topic_id: DANBOORU_TOPIC_ID, wiki_page: GITHUB_WIKI_PAGE, settings: [{ name: 'general', }, { name: 'last_noted', }, { name: 'main', }, { name: 'constructs', }, { name: 'embedded', }, { name: 'controls', }, { name: 'codes', }], controls: [], }; //CSS constants const PROGRAM_CSS = ` /** General **/ .ta-header { font-size: 1.4em; font-weight: bold; margin-bottom: 0.5em; } .ta-subheader { font-size: 1.2em; font-weight: bold; } .ta-text-input label { font-weight: bold; display: inline-block; width: 10em; } .ta-text-input input { width: 10em; height: 1.75em; } .ta-button-svg { position: relative; } .ta-button-svg img { position: absolute; } .ta-menu-tab { border: 1px solid #888; border-radius: 0.7em 0.7em 0 0; background: var(--subnav-menu-background-color); padding: 0.5em; margin: 0 -0.2em; display: inline-block; } .ta-menu-tab.ta-active { color: white; background: blue; text-shadow: 1px 0px 0px; } /** Side menu **/ #ta-side-menu { position: fixed; top: clamp(1rem, 100vh - 54.5rem, 8rem); left: 0.7em; width: 20.6em; height: auto; z-index: 100; background: var(--body-background-color); } #ta-side-menu > div { position: relative; border: 1px solid var(--text-color); padding: 0.35em; } #ta-side-menu #ta-side-menu-header { font-size: 1.4em; font-weight: bold; text-decoration: underline; margin-bottom: 0.75em; letter-spacing: -1px; transform: scaleX(0.95); margin-left: -0.4em; margin-bottom: 4.5em; } #ta-side-menu #ta-side-menu-text { position: absolute; top: 3.3em; font-size: 0.85em; border: 1px dashed #DDD; padding: 0.35em; min-height: 5em; line-height: 1.4em; width: 23em; } #ta-side-menu #ta-embedded-status-text { font-weight: bold; font-variant: small-caps; } #ta-side-menu .ta-control-button { position: absolute; top: 0.25em; padding: 0.25em; font-weight: bold; font-size: 1em; } #ta-side-menu #ta-side-menu-close { right: 0.25em; } #ta-side-menu #ta-side-menu-reset { right: 4em; } #ta-side-menu #ta-size-controls { position: absolute; top: 2.75em; right: 0.5em; padding: 0.25em 0.75em; background: #f0f0f0; } #ta-side-menu #ta-size-controls img { width: 1.5em; } #ta-side-menu #ta-side-menu-tabs { letter-spacing: -1px; border-bottom: 1px solid #F0F0F0; } #ta-side-menu button { font-weight: bold; } /** Sections **/ #ta-sections > div { font-size: 0.85em; padding: 0.35em; } #ta-sections .ta-subsection { padding-left: 0.5em; margin-bottom: 1em; display: inline-block; } #ta-sections button { font-size: 1em; padding: 0.5em 0.9em; } #ta-sections hr { border: 1px solid var(--default-border-color); } /**** Main section ****/ /****** Block subsection ******/ button.ta-html-style-tag { background-color: cadetblue; border-color: darkcyan; color: white; } button.ta-html-style-tag:hover { background-color: cadetblue; filter: brightness(1.25); } /****** Styles subsection ******/ #ta-main-styles-subsection .ta-text-input { line-height: 1em; } /**** Constructs section ****/ /****** Text shadow subsection ******/ #ta-constructs-text-shadow-subsection #ta-text-shadow-attribs { margin-left: 1em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-attribs .ta-text-input label { font-size: 1.2em; width: 6em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-attribs .ta-text-input input { width: 10em; height: 1.75em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-grid-controls { display: flex; } #ta-constructs-text-shadow-subsection #ta-text-shadow-controls { margin: 1em 1em 0 0 } #ta-constructs-text-shadow-subsection #ta-text-shadow-controls a { display: block; font-size: 1.5em; padding: 0.2em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-grid { border: 1px solid #CCC; margin-top: 1em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-grid .ta-grid-item { position: absolute; width: 2em; height: 2em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-options { margin-top: 1em; } #ta-constructs-text-shadow-subsection #ta-text-shadow-options label { font-size: 1.35em; font-weight: bold; padding-right: 1em; } /****** Ruby subsection ******/ #ta-constructs-ruby-subsection ruby { font-size: 1.5em; border: 1px solid var(--form-button-border-color); padding: 0.6em 0.2em 0.1em; } #ta-constructs-ruby-subsection #ta-ruby-text { margin: 0.25em 3em 1em 1em; padding: 2em 1em 1em; background-color: var(--subnav-background-color); border: 1px solid var(--footer-border-color); } #ta-constructs-ruby-subsection #ta-ruby-dialog-open { width: 90%; font-size: 1.25em; font-weight: bold; letter-spacing: 0.1em; } /**** Embedded section ****/ #ta-section-embedded #ta-embedded-mode { font-size: 1.2em; padding: 5px; border: 4px dashed var(--default-border-color); margin: 1em; width: 15.5em; box-shadow: 0 0 0 4px var(--subnav-menu-background-color); background: var(--subnav-menu-background-color); } /****** Embedded block subsection ******/ #ta-embedded-block-subsection { font-size: 1.2em; } #ta-embedded-block-subsection button { font-weight: bold; } #ta-embedded-block-subsection #ta-embedded-actions { margin-bottom: 1em; padding-left: 0.35em; } #ta-embedded-block-subsection #ta-embedded-level { margin-bottom: 1em; padding-left: 0.35em; } #ta-embedded-block-subsection #ta-embedded-level label { font-weight: bold; margin-right: 1em; } #ta-embedded-block-subsection #ta-embedded-level-select { padding: 0 1em; } /**** Controls section ****/ #ta-section-controls button { margin: 4px; display: inline-block; vertical-align: top; } #ta-section-controls button img { width: 2em; } /****** Placement subsection ******/ #ta-controls-placement-subsection #ta-placement-controls { padding-left: 0.5em; width: 16em; } #ta-controls-placement-subsection #ta-placement-controls button { width: 4em; height: 4em; } #ta-controls-placement-subsection #ta-placement-controls button div { font-size: 2em; } #ta-controls-placement-subsection #ta-placement-info { border: 1px solid var(--footer-border-color); padding: 5px 5px 0 5px; } #ta-controls-placement-subsection #ta-placement-info > div:not(:nth-last-child(1)) { padding-bottom: 0.6em; } #ta-controls-placement-subsection #ta-placement-info span { font-size: 0.8em; } /****** Actions subsection ******/ #ta-controls-actions-subsection button { font-weight: bold; width: 5.15em; font-size: 1.2em; padding: 0.5em 0em; } /**** Codes section ****/ #ta-section-codes button { font-size: 1.25em; font-weight: bold; width: 3.03em; padding: 0.28em; margin: 2px; } /****** HTML characters subsection ******/ #ta-section-codes #ta-codes-html-subsection button { width: 2.55em; } /****** Special characters subsection ******/ #ta-section-codes #ta-codes-special-subsection button { font-size: 1.5em; width: 2.13em; height: 2.13em; } /** Menu options **/ #ta-menu-options { margin-bottom: 1em; } #ta-menu-options > div { display: inline-block; position: relative; width: 9em; height: 1em; } #ta-menu-options label { font-weight: bold; position: absolute; } #ta-menu-options input { position: absolute; } #ta-css-style-overwrite { left: 7em; top: 0.15em; } #ta-css-style-initialize { left: 7em; top: 0.15em; } /** Menu controls **/ #ta-menu-controls button { font-size: 0.9em; padding: 0.25em 1em; } /** Ruby dialog **/ #ta-ruby-dialog { display: flex; } #ta-ruby-styles { margin-right: 2em; } #ta-ruby-dialog-tabs { border-bottom: 1px solid #DDD; margin-bottom: 0.5em; } #ta-ruby-editor { width: 55%; } #ta-ruby-editor > div:not(:nth-last-child(1)) { margin-bottom: 0.6em; } #ta-ruby-editor .ta-ruby-textarea textarea { height: 7em; } /** Load dialog **/ .ta-load-message ul { font-size: 90%; list-style: disc; } .ta-load-saved-controls { margin-bottom: 1em; } .ta-load-sessions { height: 26em; border: 1px solid #DDD; overflow-y: auto; overflow-x: hidden; } .ta-load-sessions ul { margin: 0 !important; } .ta-load-sessions li { white-space: nowrap; } .ta-load-sessions label { padding: 5px; } .ta-load-session-item { padding: 5px; display: inline-block; } /** Post options **/ #ta-side-menu-open { color: green; } /** Cursor **/ #ta-side-menu button[disabled], #ta-ruby-dialog ~ div button[disabled] { cursor: default; } #ta-side-menu *:not(a, button, input, select) { cursor: move; } #ta-side-menu .ta-cursor-initial, #ta-side-menu .ta-cursor-initial *:not(a, button) { cursor: initial; } #ta-side-menu .ta-cursor-text, #ta-side-menu .ta-cursor-text * { cursor: text; } #ta-side-menu .ta-cursor-pointer, #ta-side-menu .ta-cursor-pointer *, #ta-side-menu button { cursor: pointer; } /** Focus **/ #ta-main-blocks-subsection button:focus-visible, #ta-main-styles-subsection input:focus-visible, #ta-text-shadow-attribs input:focus-visible, #ta-embedded-style-subsection input:focus-visible, #ta-ruby-dialog input:focus-visible { position: relative; /* Hack so that the focus border isn't clobbered by neighbors (e.g. on Firefox) */ }`; const MENU_CSS = ` .jsplib-selectors.ta-selectors:not([data-setting="available_html_tags"], [data-setting="domain_selector"]) label { width: 165px; }`; //HTML constants const EXPAND_LR_SVG = ''; const EXPAND_TB_SVG = ''; const CONTRACT_LR_SVG = ''; const CONTRACT_TB_SVG = ''; const PLUS_SIGN = ` `; const MINUS_SIGN = ` `; const SIDE_MENU = `
`; const TEXT_SHADOW_SUBSECTION = `' + JSPLib.utility.HTMLEscape(line.slice(message.firstColumn - 1, message.lastColumn)) + '
';
row = message.lastLine - 1;
column = message.firstColumn;
} else {
highlight_html = row = column = 'N/A';
}
return `
${highlight_html}
validator.nu
");
JSPLib.debug.debugerror("Server error:", e);
return null;
}
try {
data = JSON.parse(resp.response);
} catch(e) {
JSPLib.notice.error("Unable to parse validation response!");
JSPLib.debug.debugerror("Parse error:", e, resp.response);
return null;
}
if (!JSPLib.validate.isHash(data) || !('messages' in data)) {
JSPLib.notice.error("Unexpected response format!");
JSPLib.debug.debugerror("Unexpected format:", data);
return null;
}
return data;
}
//// HTML functions
function IsInsideHTMLTag(html_text, cursor) {
let c = cursor;
let d = cursor - 1;
return (((html_text.indexOf('<', c) < 0) && (html_text.indexOf('>', c) >= 0)) || (html_text.indexOf('<', c) > html_text.indexOf('>', c))) &&
(((html_text.lastIndexOf('>', d) < 0) && (html_text.lastIndexOf('<', d) >= 0)), (html_text.lastIndexOf('>', d) < html_text.lastIndexOf('<', d)));
}
function BuildHTMLTag(tag_name, attrib_dict, style_dict, blank_style = false) {
let style_pairs = Object.entries(style_dict).filter((style_pair) => (style_pair[1] !== ""));
if (style_pairs.length){
attrib_dict.style = style_pairs.map((style_pair) => (style_pair[0] + ": " + style_pair[1])).join('; ') + ';';
} else if (blank_style) {
attrib_dict.style = "";
} else {
delete attrib_dict.style;
}
let attrib_html = Object.entries(attrib_dict).map((attrib_pair) => (attrib_pair[0] + '="' + attrib_pair[1] + '"')).join(' ');
attrib_html = (attrib_html ? " " : "") + attrib_html;
return '<' + tag_name + attrib_html + '>';
}
function ParseTagAttributes(html_tag) {
let attrib_items = JSPLib.utility.findAll(html_tag, /\w+="[^"]+"/g);
let attrib_pairs = attrib_items.map((attrib) => JSPLib.utility.findAll(attrib, /(\w+)="([^"]+)"/g).filter((_item, i) => (i % 3)));
let attrib_dict = Object.assign({}, ...attrib_pairs.map((attrib_pair) => ({[attrib_pair[0]]: attrib_pair[1]})));
var style_dict;
if ('style' in attrib_dict) {
let style_pairs = attrib_dict.style.split(';').filter((style) => (!style.match(/^\s*$/))).map((style) => (style.split(':').map((str) => str.trim())));
style_dict = Object.assign({}, ...style_pairs.map((style) => ({[style[0]]: style[1]})));
} else {
style_dict = {};
}
JSPLib.debug.debuglog("ParseTagAttributes", {attrib_dict, style_dict});
return {attrib_dict, style_dict};
}
function TokenizeHTML(html_string) {
if (TokenizeHTML.tags) return TokenizeHTML.tags;
let html_length = html_string.length;
let html_tags = [];
let tag_stack = [];
let unclosed_tags = [];
let position = 0;
let match = null;
while (match = html_string.match(HTML_REGEX)) {
let start_pos = position + match.index;
let end_pos = start_pos + match[0].length;
if (match[1]) {
let html_tag = tag_stack.pop();
if (html_tag) {
if (html_tag.tag_name === match[2]) {
html_tag.close_tag_start = start_pos;
html_tag.close_tag_end = end_pos;
unclosed_tags.forEach((tag) => {
tag.close_tag_start = tag.close_tag_end = start_pos;
});
unclosed_tags.length = 0;
} else {
unclosed_tags.push(html_tag);
}
}
} else {
let html_tag = {
tag_name: match[2],
open_tag_start: start_pos,
open_tag_end: end_pos
};
html_tags.push(html_tag);
tag_stack.push(html_tag);
}
let increment = match.index + match[0].length;
position += increment;
html_string = html_string.slice(increment);
}
tag_stack.concat(unclosed_tags).forEach((tag) => {
tag.close_tag_start = tag.close_tag_end = html_length;
});
html_tags.forEach((tag) => {
tag.ancestor_tags = html_tags.filter((outer_tag) => {
if (tag === outer_tag) return false;
return outer_tag.open_tag_end <= tag.open_tag_start && outer_tag.close_tag_start >= tag.close_tag_end;
});
tag.descendant_tags = html_tags.filter((inner_tag) => {
if (tag === inner_tag) return false;
return tag.open_tag_end <= inner_tag.open_tag_start && tag.close_tag_start >= inner_tag.close_tag_end;
});
});
return html_tags;
}
function GetParentTag(html_tags, cursor) {
let ancestor_tags = html_tags.filter((tag) => tag.open_tag_end <= cursor && tag.close_tag_start >= cursor);
return ancestor_tags.reduce((acc, tag) => (!acc || (tag.ancestor_tags.length > acc.ancestor_tags.length) ? tag : acc), null);
}
function GetTag(html_text, cursor, warning = true) {
let tag;
if (TA.mode === 'main' || TA.mode === 'constructs') {
tag = GetHTMLTag(html_text, cursor);
if (!tag && warning) {
ShowErrorMessages(["No open tag selected."]);
}
} else if (TA.mode === 'embedded') {
tag = GetEmbeddedTag(html_text);
if (!tag && warning) {
ShowErrorMessages(["No tag with class note-box-attributes
."]);
}
} else if (warning) {
ShowErrorMessages([`Mode ${TA.mode}
not implement for current function.`]);
}
return tag;
}
function GetHTMLTag(html_text, cursor) {
let html_tags = TokenizeHTML(html_text);
let html_tag = html_tags.filter((tag) => (cursor >= tag.open_tag_start && cursor < tag.open_tag_end))[0];
if (!html_tag) return;
html_tag.open_tag = html_text.slice(html_tag.open_tag_start, html_tag.open_tag_end);
html_tag.close_tag = html_text.slice(html_tag.close_tag_start, html_tag.open_tag_end);
html_tag.inner_html = html_text.slice(html_tag.open_tag_end, html_tag.close_tag_start);
html_tag.full_tag = html_text.slice(html_tag.open_tag_start, html_tag.close_tag_end);
html_tag.tag_name = html_tag.open_tag.match(/<(\w+)/)[1];
Object.assign(html_tag, ParseTagAttributes(html_tag.open_tag));
JSPLib.debug.debuglog("GetHTMLTag", html_tag);
return html_tag;
}
function GetEmbeddedTag(html_text) {
let html_tags = TokenizeHTML(html_text);
let embedded_tag = html_tags.find((html_tag) => {
html_tag.open_tag = html_text.slice(html_tag.open_tag_start, html_tag.open_tag_end);
if (!html_tag.open_tag.match(/ class="[^"]+"/)) return;
Object.assign(html_tag, ParseTagAttributes(html_tag.open_tag));
if (html_tag.attrib_dict["class"].split(' ').includes('note-box-attributes')) return html_tag;
});
if (!embedded_tag) return;
embedded_tag.close_tag = html_text.slice(embedded_tag.close_tag_start, embedded_tag.open_tag_end);
embedded_tag.inner_html = html_text.slice(embedded_tag.open_tag_end, embedded_tag.close_tag_start);
embedded_tag.full_tag = html_text.slice(embedded_tag.open_tag_start, embedded_tag.close_tag_end);
embedded_tag.tag_name = embedded_tag.open_tag.match(/<(\w+)/)[1];
JSPLib.debug.debuglog("GetEmbeddedTag", embedded_tag);
return embedded_tag;
}
function GetRubyTag(html_text, cursor) {
let html_tags = TokenizeHTML(html_text);
let overall_ruby_tag = html_tags.find((html_tag) => {
if (html_tag.open_tag_start > cursor || html_tag.open_tag_end <= cursor) return;
html_tag.open_tag = html_text.slice(html_tag.open_tag_start, html_tag.open_tag_end);
html_tag.tag_name = html_tag.open_tag.match(/<(\w+)/)[1];
if (html_tag.tag_name !== 'ruby') return;
html_tag.close_tag = html_text.slice(html_tag.close_tag_start, html_tag.open_tag_end);
html_tag.inner_html = html_text.slice(html_tag.open_tag_end, html_tag.close_tag_start);
html_tag.full_tag = html_text.slice(html_tag.open_tag_start, html_tag.close_tag_end);
Object.assign(html_tag, ParseTagAttributes(html_tag.open_tag));
return html_tag;
});
if (!overall_ruby_tag) return;
let inner_tags = html_tags.filter((html_tag) => ((html_tag.open_tag_start >= overall_ruby_tag.open_tag_end) && (html_tag.close_tag_end <= overall_ruby_tag.close_tag_start)))
.sort((a, b) => (a.open_tag_start - b.open_tag_start));
let temp_inner_tags = [...inner_tags];
let base_inner_tags = [];
var next_inner_tag;
const _unshift_tags = function (current_tag, html_tags) {return html_tags.filter((html_tag) => (html_tag.open_tag_start >= next_inner_tag.close_tag_end));};
while (next_inner_tag = inner_tags.shift()) {
inner_tags = _unshift_tags(next_inner_tag, inner_tags);
next_inner_tag.open_tag = html_text.slice(next_inner_tag.open_tag_start, next_inner_tag.open_tag_end);
next_inner_tag.tag_name = next_inner_tag.open_tag.match(/<(\w+)/)[1];
if (next_inner_tag.tag_name !== 'rt' && next_inner_tag.tag_name !== 'span') continue;
next_inner_tag.close_tag = html_text.slice(next_inner_tag.close_tag_start, next_inner_tag.open_tag_end);
next_inner_tag.inner_html = html_text.slice(next_inner_tag.open_tag_end, next_inner_tag.close_tag_start);
next_inner_tag.full_tag = html_text.slice(next_inner_tag.open_tag_start, next_inner_tag.close_tag_end);
Object.assign(next_inner_tag, ParseTagAttributes(next_inner_tag.open_tag));
base_inner_tags.push(next_inner_tag);
}
let top_ruby_tags = base_inner_tags.filter((html_tag) => (html_tag.tag_name === 'rt'));
let bottom_ruby_tags = base_inner_tags.filter((html_tag) => (html_tag.tag_name === 'span'));
return {overall: overall_ruby_tag, top: top_ruby_tags, bottom: bottom_ruby_tags, temp_inner_tags, html_tags};
}
function ValidateCSS(input_html) {
let $validator = $('');
let valid_styles = Object.keys($validator[0].style).map(JSPLib.utility.kebabCase);
let error_array = [];
for (let match of input_html.matchAll(/(style\s*=\s*")([^"]+)"/g)) {
let style_index = match.index + match[1].length + 1; // One-based positioning
let styles = match[2].replace(/\s*;\s*$/, '').split(';'); // Remove the final semi-colon
for (let i = 0; i < styles.length; style_index += styles[i++].length + 1) {
let error = {
excerpt: styles[i],
index: style_index,
};
let [attr, value, ...misc] = styles[i].split(':');
if (misc.length) {
error_array.push(Object.assign({message: "Extra colons found."}, error));
continue;
}
attr = attr.trim();
if (value === undefined) {
if (attr.length === 0) {
error.excerpt += ';';
error_array.push(Object.assign({message: "Extra-semi colon found."}, error));
} else {
error_array.push(Object.assign({message: "No colons found."}, error));
}
continue;
}
if (!valid_styles.includes(attr)) {
error_array.push(Object.assign({message: "Invalid style attribute: " + attr}, error));
continue;
}
let attr_key = JSPLib.utility.camelCase(attr);
value = value.replace(/\s*!important\s*$/, "").trim();
$validator[0].style[attr_key] = value;
if ($validator[0].style[attr_key] === "") {
error_array.push(Object.assign({message: "Invalid style value: " + value}, error));
}
}
}
return error_array;
}
// DOM functions
function UpdateHTMLStyles(text_area, add_styles) {
//Add styles to an existing HTML tag
let html_tag = GetTag(text_area.value, text_area.selectionStart);
if (!html_tag) return;
let style_dict = MergeCSSStyles(html_tag.style_dict, add_styles);
let final_tag = BuildHTMLTag(html_tag.tag_name, html_tag.attrib_dict, style_dict);
text_area.value = text_area.value.slice(0, html_tag.open_tag_start) + final_tag + text_area.value.slice(html_tag.open_tag_end);
text_area.focus();
text_area.setSelectionRange(html_tag.open_tag_start, html_tag.open_tag_start + final_tag.length);
}
function InsertHTMLBlock(text_area, tag_name, style_dict) {
//Insert a new HTML tag
let prefix = BuildHTMLTag(tag_name, {}, style_dict);
let suffix = '' + tag_name + '>';
let fixtext = "";
let select_start = text_area.selectionStart;
fixtext = text_area.value.slice(0, text_area.selectionStart);
let valueselection = text_area.value.slice(text_area.selectionStart, text_area.selectionEnd);
fixtext += prefix + valueselection + suffix;
let caretPos = fixtext.length;
fixtext += text_area.value.slice(text_area.selectionEnd);
text_area.value = fixtext;
text_area.focus();
text_area.setSelectionRange(select_start, caretPos);
}
function AddBlockElement(text_area, tag_name) {
let html_text = text_area.value;
let cursor = text_area.selectionStart;
let cursor_end = text_area.selectionEnd;
if (IsInsideHTMLTag(html_text, cursor_end)) {
ShowErrorMessages(["Cannot end selection inside another tag."]);
return;
}
let html_tags = TokenizeHTML(html_text);
if (GetParentTag(html_tags, cursor) !== GetParentTag(html_tags, cursor_end)) {
ShowErrorMessages(["Selection end cannot end inside a sibling tag."]);
return;
}
let initialize = $('#ta-css-style-initialize').get(0)?.checked;
let [create_styles, invalid_styles] = (initialize && HTML_STYLE_TAGS.includes(tag_name) ? GetCSSStyles(false, INPUT_SECTIONS[TA.mode]) : [{}, {}]);
InsertHTMLBlock(text_area, tag_name, create_styles);
let style_errors = Object.entries(invalid_styles).map((style_pair) => (`${style_pair[0]}
=> "${style_pair[1]}"`));
if (style_errors.length) {
ShowStyleErrors(style_errors);
} else {
TA.$close_notice_link.click();
}
}
function ChangeBlockElement(text_area, tag_name) {
let cursor = text_area.selectionStart;
let html_string = text_area.value;
let html_tags = TokenizeHTML(html_string);
let html_tag = html_tags.filter((tag) => ((cursor > tag.open_tag_start && cursor < tag.open_tag_end) || (cursor > tag.close_tag_start && cursor < tag.close_tag_end)))[0];
if (!html_tag) return;
if (html_tag.close_tag_start) {
//Open tags may not have a close tag
let end_tag = html_string.slice(html_tag.close_tag_start, html_tag.close_tag_end);
end_tag = end_tag.replace(HTML_REGEX, `<$1${tag_name}$3>`);
html_string = html_string.slice(0, html_tag.close_tag_start) + end_tag + html_string.slice(html_tag.close_tag_end);
}
let start_tag = html_string.slice(html_tag.open_tag_start, html_tag.open_tag_end);
start_tag = start_tag.replace(HTML_REGEX, `<$1${tag_name}$3>`);
html_string = html_string.slice(0, html_tag.open_tag_start) + start_tag + html_string.slice(html_tag.open_tag_end);
text_area.value = html_string;
text_area.focus();
let start_pos = html_tag.open_tag_start + 1;
text_area.setSelectionRange(start_pos, start_pos + tag_name.length);
TA.$close_notice_link.click();
}
function DeleteBlockElement(text_area) {
let cursor = text_area.selectionStart;
let html_string = text_area.value;
let html_tags = TokenizeHTML(html_string);
let html_tag = html_tags.filter((tag) => ((cursor > tag.open_tag_start && cursor < tag.open_tag_end) || (cursor > tag.close_tag_start && cursor < tag.close_tag_end)))[0];
if (!html_tag) return;
if (html_tag.close_tag_start) {
//Open tags may not have a close tag
html_string = html_string.slice(0, html_tag.close_tag_start) + html_string.slice(html_tag.close_tag_end);
}
html_string = html_string.slice(0, html_tag.open_tag_start) + html_string.slice(html_tag.open_tag_end);
text_area.value = html_string;
text_area.focus();
text_area.setSelectionRange(cursor, cursor);
TA.$close_notice_link.click();
}
//// Input functions
function GetInputs(key) {
let save_data = {};
let selector = INPUT_SECTIONS[key];
$(selector).each((_i, input) => {
if (input.tagName === 'INPUT' && input.type === 'checkbox') {
save_data[input.name] = input.checked;
} else {
save_data[input.name] = input.value;
}
});
return save_data;
}
function SetInputs(key, load_data) {
let selector = INPUT_SECTIONS[key];
$(selector).each((_i, input) => {
if (!(input.name in load_data)) return;
if (input.tagName === 'INPUT' && input.type === 'checkbox') {
input.checked = load_data[input.name];
} else {
input.value = load_data[input.name];
}
});
}
function SaveMenuState() {
for (let key in INPUT_SECTIONS) {
TA.save_data = Object.assign(TA.save_data, GetInputs(key));
}
JSPLib.storage.setStorageData('ta-saved-inputs', TA.save_data, localStorage);
JSPLib.storage.setStorageData('ta-mode', TA.mode, localStorage);
let {left, top, fontSize} = $('#ta-side-menu').get(0).style;
JSPLib.storage.setStorageData('ta-position', {left, top, fontSize}, localStorage);
}
function ClearInputs(selector) {
$(selector).each((_i, input) => {
input.value = "";
});
}
function GetActiveTextArea(close_notice = true) {
let text_area = $('.note-edit-dialog').filter((_i, entry) => (entry.style.display !== 'none')).find('textarea').get(0);
if (!text_area) {
JSPLib.notice.error("No active note edit box!");
return;
}
if (close_notice) {
TA.$close_notice_link.click();
}
TokenizeHTML.tags = null;
return text_area;
}
// Note functions
function ReloadNotes() {
Danbooru.Note.notes.clear();
$('.note-box, .note-body').remove();
Danbooru.Note.load_all();
Danbooru.Note.Box.scale_all();
}
function GetMovableNote() {
let note = [...Danbooru.Note.notes].filter((note) => note.is_selected())[0];
if (!note) {
JSPLib.notice.error("No selected note!");
} else {
TA.$close_notice_link.click();
}
return note;
}
function GetAllNotesOrdered() {
let [new_notes, saved_notes] = [...Danbooru.Note.notes].reduce((total, note) => ((note.id === null ? total[0].push(note) : total[1].push(note)) && total), [[], []]);
saved_notes.sort((a, b) => (a.id - b.id));
return JSPLib.utility.concat(saved_notes, new_notes);
}
function GetNotePlacement(note) {
$('#ta-placement-info-x').text(note.x);
$('#ta-placement-info-y').text(note.y);
$('#ta-placement-info-w').text(note.w);
$('#ta-placement-info-h').text(note.h);
}
function SetNotePlacement(note) {
note.box.place_note(note.x, note.y, note.w, note.h, true);
Danbooru.Note.Body.hide_all();
note.box.$note_box.addClass("unsaved");
}
function SelectNote(callback) {
if (Danbooru.Note.notes.size === 0) {
JSPLib.notice.error("No notes to select!");
}
let all_notes = GetAllNotesOrdered();
let current_note = all_notes.filter((note) => note.is_selected())[0];
let select_note = callback(current_note, all_notes);
if (current_note) {
current_note.unselect();
}
select_note.select();
select_note.box.$note_box[0].scrollIntoView({
behavior: 'auto',
block: 'center',
inline: 'nearest'
});
if (!TA.has_embedded) {
select_note.body.show();
}
}
// CSS functions
function GetCSSStyles(overwrite, selector) {
let add_styles = {};
let invalid_styles = {};
let test_div = document.createElement('div');
$(selector).each((_i, input) => {
let value = input.value.trim(/\s/);
if (value === "" && !overwrite) return;
let style_name = input.dataset.name;
let [parse_style_name, parse_value] = STYLE_CONFIG[style_name]?.parse?.(style_name, value) || [style_name, value];
let normalized_value = STYLE_CONFIG[style_name]?.normalize?.(parse_value) || parse_value;
let use_style_name = (STYLE_CONFIG[style_name]?.use_parse ? parse_style_name : style_name);
test_div.style.setProperty(use_style_name, normalized_value);
if (test_div.style.getPropertyValue(use_style_name) === normalized_value) {
let final_value = STYLE_CONFIG[style_name]?.finalize?.(parse_value) || parse_value;
add_styles[parse_style_name] = final_value;
} else {
JSPLib.debug.debugwarn(`Invalid style [${style_name}]: ${value} => ${parse_value} -> ${normalized_value} != ${test_div.style.getPropertyValue(style_name)}`);
invalid_styles[parse_style_name] = parse_value;
}
});
JSPLib.debug.debuglog('GetCSSStyles', {add_styles, invalid_styles});
return [add_styles, invalid_styles];
}
function MergeCSSStyles(style_dict, add_styles) {
let copy_style_dict = JSPLib.utility.dataCopy(style_dict);
let copy_keys = Object.keys(copy_style_dict);
for (let style_name in add_styles) {
copy_keys.forEach((key) => {
if (STYLE_CONFIG[style_name]?.family?.includes(key)) {
delete copy_style_dict[key];
}
});
copy_style_dict[style_name] = add_styles[style_name];
}
return copy_style_dict;
}
//// CSS color
function ValidateColor(text) {
let test_div = document.createElement('div');
let normalized_color = NormalizeColor(text);
test_div.style.color = normalized_color;
return test_div.style.color === normalized_color;
}
function NormalizeColor(text) {
var match;
if (match = text.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i)) {
text = '#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3];
} else if (match = text.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])$/i)) {
text = '#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3] + match[4] + match[4];
}
if (match = text.match(/^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$/i)) {
let [r, g, b] = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
return `rgb(${r}, ${g}, ${b})`;
}
if (match = text.match(/^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$/i)) {
let [r, g, b, a] = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16), parseFloat((parseInt(match[4], 16) / 255).toFixed(3))];
if (a === 1) {
return `rgb(${r}, ${g}, ${b})`;
}
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return FinalizeColor(text);
}
function FinalizeColor(text) {
var match;
let compressed_text = text.split(/\s+/).join('');
if (match = compressed_text.match(/^rgb\(([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3})\)$/)) {
return `rgb(${match[1]}, ${match[2]}, ${match[3]})`;
}
if (match = compressed_text.match(/^rgba\(([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9.]{1,5})\)$/)) {
if (parseFloat(match[4]) === 1) {
return `rgb(${match[1]}, ${match[2]}, ${match[3]})`;
}
return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${match[4]})`;
}
return text;
}
//// CSS size
function ValidateSize(text) {
let test_div = document.createElement('div');
let normalized_size = NormalizeSize(text);
test_div.style.top = normalized_size;
return test_div.style.top === normalized_size;
}
function NormalizeSize(text) {
return text.replace(/(?<=^|\s)0(?=$|\s)/g, '0px');
}
//// CSS direction
function ParseDirection(style_name, text) {
let match = text.match(/^\s*(top|bottom|left|right)\s+(\S+)/);
return (match ? [style_name + '-' + match[1], match[2]] : [style_name, text]);
}
function CopyDirection(style_name, text) {
let [ , name, direction] = style_name.match(/(\S+)-(top|bottom|left|right)/);
return [name, direction + ' ' + text];
}
function ParseDirectionStyles(style_dict) {
let copy_style_dict = {};
for (let style_name in style_dict) {
let [copy_style_name, copy_value] = STYLE_CONFIG[style_name]?.copy?.(style_name, style_dict[style_name]) || [];
if (copy_style_name) {
copy_style_dict[copy_style_name] = copy_value;
} else {
copy_style_dict[style_name] = style_dict[style_name];
}
}
return copy_style_dict;
}
//// CSS text shadow
function BuildTextShadowStyle(append, style_dict) {
let errors = [];
let attribs = Object.assign(...$('#ta-text-shadow-attribs input').map((i, entry) => ({[entry.dataset.name.trim()]: entry.value})));
let initial_shadow = (append && style_dict['text-shadow']) || "";
if (attribs.size === "") {
return initial_shadow;
}
if (!ValidateSize(attribs.size)) errors.push("Invalid size specified.");
if ((attribs.color !== "") && !ValidateColor(attribs.color)) errors.push("Invalid color specified.");
if ((attribs.blur !== "") && !ValidateSize(attribs.blur)) errors.push("Invalid blur specified.");
if (errors.length) {
JSPLib.notice.error(errors.join('${style_pair[0]}
=> "${style_pair[1]}"`));
if (Object.keys(add_styles).length) {
UpdateHTMLStyles(text_area, add_styles);
} else if (!overwrite && style_errors.length === 0) {
ShowErrorMessages(["No styles entered."]);
return;
}
if (style_errors.length) {
ShowStyleErrors(style_errors);
} else {
TA.$close_notice_link.click();
}
}
function ClearTagStyles() {
ClearInputs(INPUT_SECTIONS[TA.mode]);
}
function LoadTagStyles() {
OpenLoadDialog(TA.mode);
}
//// Main section handlers
function ApplyBlock(event) {
let text_area = GetActiveTextArea(false);
if (!text_area) return;
SaveHTML(text_area);
if (IsInsideHTMLTag(text_area.value, text_area.selectionStart)) {
ChangeBlockElement(text_area, event.currentTarget.value);
} else {
AddBlockElement(text_area, event.currentTarget.value);
}
}
function DeleteBlock() {
let text_area = GetActiveTextArea(false);
if (!text_area) return;
if (IsInsideHTMLTag(text_area.value, text_area.selectionStart)) {
SaveHTML(text_area);
DeleteBlockElement(text_area);
} else {
JSPLib.notice.error("No tag selected!");
}
}
function SaveHTML(text_area) {
let $text_area = $(text_area);
let undo_actions = $text_area.data('undo_actions') || [];
let undo_index = $text_area.data('undo_index') || 0;
undo_actions = undo_actions.slice(0, undo_index);
undo_actions.push(text_area.value);
$text_area.data('undo_actions', undo_actions);
$text_area.data('undo_index', undo_actions.length);
$text_area.data('undo_saved', true);
JSPLib.debug.debuglog('SaveMarkup', {undo_actions, undo_index});
}
function UndoAction() {
let text_area = GetActiveTextArea(false);
if (!text_area) return;
let $text_area = $(text_area);
let {undo_actions = [], undo_index = 0, undo_saved} = $text_area.data();
if (undo_saved) {
undo_actions.push(text_area.value);
$text_area.data('undo_actions', undo_actions);
}
let undo_html = undo_actions.slice(undo_index - 1, undo_index)[0];
if (JSPLib.validate.isString(undo_html)) {
text_area.value = undo_html;
} else {
JSPLib.notice.notice("Beginning of actions buffer reached.");
}
let new_index = Math.max(0, undo_index - 1);
$text_area.data('undo_index', new_index);
$text_area.data('undo_saved', false);
JSPLib.debug.debuglog('UndoAction', {undo_actions, undo_index, new_index});
return Boolean(undo_html);
}
function RedoAction() {
let text_area = GetActiveTextArea(false);
if (!text_area) return;
let $text_area = $(text_area);
let {undo_actions = [], undo_index = 0} = $text_area.data();
let undo_html = undo_actions.slice(undo_index + 1, undo_index + 2)[0];
if (undo_html) {
text_area.value = undo_html;
} else {
JSPLib.notice.notice("End of actions buffer reached.");
}
let new_index = Math.min(undo_actions.length - 1, undo_index + 1);
$text_area.data('undo_index', new_index);
$text_area.data('undo_saved', false);
JSPLib.debug.debuglog('RedoAction', {undo_actions, undo_index, new_index});
return Boolean(undo_html);
}
function ClearActions(event) {
let $text_area = $(event.currentTarget);
$text_area.data('undo_actions', []);
$text_area.data('undo_index', 0);
$text_area.data('undo_saved', false);
JSPLib.debug.debuglog('Cleared actions.');
}
function NormalizeNote() {
let text_area = GetActiveTextArea();
if (!text_area) return;
SaveHTML(text_area);
let html_text = text_area.value;
let normalized_text = $('').replace(/<\/tn>/g, '
'); let html_errors = await ValidateHTML(transform_html); if (html_errors === null) { return; } let error_lines = []; if (html_errors.messages.length) { JSPLib.debug.debuglog("HTML errors:", html_errors); error_lines = html_errors.messages.map((message, i) => RenderHTMLError(i, message, transform_html)); } let css_errors = ValidateCSS(text_area.innerHTML); if (css_errors.length) { JSPLib.debug.debuglog("CSS errors:", css_errors); error_lines = JSPLib.utility.concat(error_lines, css_errors.map((error, i) => RenderCSSError(i, error))); } if (error_lines.length) { JSPLib.notice.error('note-box-attributes
already exists.']);
return;
}
let note_html = (html_tag ? html_text.replace(html_tag.full_tag, "") : html_text);
let initialize = $('#ta-css-style-initialize').get(0)?.checked;
let [add_styles, invalid_styles] = (initialize ? GetCSSStyles(false, INPUT_SECTIONS[TA.mode]) : [{}, {}]);
let embedded_tag = BuildHTMLTag('div', {class: 'note-box-attributes'}, add_styles, true) + '';
note_html += embedded_tag;
text_area.value = note_html;
let style_errors = Object.entries(invalid_styles).map((style_pair) => (`${style_pair[0]}
=> "${style_pair[1]}"`));
if (style_errors.length) {
ShowStyleErrors(style_errors);
} else {
TA.$close_notice_link.click();
}
}
function RemoveEmbeddedElement() {
let text_area = GetActiveTextArea();
if (!text_area) return;
SaveHTML(text_area);
let html_text = text_area.value;
let html_tag = GetTag(html_text, text_area.selectionStart);
if (!html_tag) return;
text_area.value = html_text.replace(html_tag.full_tag, "");
}
function SetEmbeddedLevel() {
let text_area = GetActiveTextArea();
if (!text_area) return;
SaveHTML(text_area);
let html_text = text_area.value;
let html_tag = GetTag(html_text, text_area.selectionStart);
if (!html_tag) return;
let classlist = html_tag.attrib_dict["class"].split(/\s+/).filter((classname) => (!classname.match(/level-[1-5]/)));
let level = $('#ta-embedded-level-select').val();
if (level.match(/^[1-5]$/)){
classlist.push('level-' + level);
}
html_tag.attrib_dict["class"] = classlist.join(' ');
let final_tag = BuildHTMLTag(html_tag.tag_name, html_tag.attrib_dict, html_tag.style_dict);
text_area.value = html_text.replace(html_tag.open_tag, final_tag);
}
//// Controls section handlers
function GetPlacement() {
let note = GetMovableNote();
if (!note) return;
GetNotePlacement(note);
}
function PlacementControl(event) {
let note = GetMovableNote();
if (!note) return;
let action = $(event.currentTarget).data('action');
switch (action) {
case 'move-up':
note.y -= 1;
break;
case 'move-down':
note.y += 1;
break;
case 'move-left':
note.x -= 1;
break;
case 'move-right':
note.x += 1;
break;
case 'contract-width':
note.w -= 1;
break;
case 'expand-width':
note.w += 1;
break;
case 'contract-height':
note.h -= 1;
break;
case 'expand-height':
note.h += 1;
//falls through
default:
//do nothing
}
SetNotePlacement(note);
GetNotePlacement(note);
}
function SaveNote() {
let note = GetMovableNote();
if (!note) return;
let params = {
x: note.x,
y: note.y,
width: note.w,
height: note.h,
};
var note_promise;
if (note.is_new()) {
// The body is only saveable this way for new notes; otherwise use the edit dialog.
params.body = note.original_body;
params.post_id = note.post_id;
note_promise = JSPLib.network.post(`/notes.json`, {data: {note: params }});
} else {
note_promise = JSPLib.network.put(`/notes/${note.id}.json`, {data: {note: params }});
}
note_promise.then(
(response) => {
if (note.is_new()) {
note.id = response.id;
}
note.box.$note_box.removeClass("unsaved");
JSPLib.notice.notice(`Note #${note.id} saved.`);
},
() => {JSPLib.notice.error(`Error saving note #${note.id}`);}
);
}
function ResetNote() {
let note = GetMovableNote();
if (!note) return;
if (!note.is_new()) {
JSPLib.network.getJSON(`/notes/${note.id}.json`).then((data) => {
note.box.place_note(data.x, data.y, data.width, data.height);
let text_area = GetActiveTextArea();
if (text_area) {
text_area.value = data.body;
}
note.body.preview_text(data.body).then(() => {
JSPLib.notice.notice(`Note #${note.id} reset.`);
note.box.$note_box.removeClass("unsaved");
});
});
} else {
JSPLib.notice.error("Reset not available for new unsaved notes.");
}
}
function DeleteNote() {
let note = GetMovableNote();
if (!note) return;
if (!note.is_new()) {
if (!confirm("Do you really want to delete this note?")) return;
JSPLib.network["delete"](`/notes/${note.id}.json`).then(
() => {JSPLib.notice.notice(`Note #${note.id} deleted.`);},
() => {JSPLib.notice.error(`Error deleting note #${note.id}.`);},
);
}
note.box.$note_box.remove();
note.body.$note_body.remove();
Danbooru.Note.notes["delete"](note);
TA.starting_notes["delete"](note.id);
}
function EditNote() {
let note = GetMovableNote();
if (!note) return;
Danbooru.Note.Edit.show(note);
}
function ShowNote() {
let note = GetMovableNote();
if (!note) return;
note.box.$note_box.show();
note.body.$note_body.show();
}
function HideNote() {
let note = GetMovableNote();
if (!note) return;
note.box.$note_box.hide();
note.body.$note_body.hide();
}
function NextNote() {
SelectNote((current_note, all_notes) => {
if (current_note) {
let next_index = (all_notes.indexOf(current_note) + 1) % Danbooru.Note.notes.size ;
return all_notes[next_index];
}
return all_notes[0];
});
}
function PreviousNote() {
SelectNote((current_note, all_notes) => {
if (current_note) {
let previous_index = all_notes.indexOf(current_note) - 1;
previous_index = (previous_index < 0 ? (Danbooru.Note.notes.size - 1) : previous_index);
return all_notes[previous_index];
}
return all_notes.slice(-1)[0];
});
}
function UnselectNote() {
let note = GetMovableNote();
if (!note) return;
note.unselect();
}
function CopyNote() {
let note = GetMovableNote();
let copy_note = new Danbooru.Note({
// Randomly place note within random width/height distance
x: JSPLib.utility.clamp(2 * note.w * (Math.random() - 0.5) + note.x, 0, note.post_width - note.w),
y: JSPLib.utility.clamp(2 * note.h * (Math.random() - 0.5) + note.y, 0, note.post_height - note.h),
w: note.w,
h: note.h,
original_body: note.original_body,
sanitized_body: (TA.has_embedded ? note.box.$inner_border.html() : note.body.$note_body.html()),
});
copy_note.select();
}
//// Codes section handlers
function InsertCharacter(event) {
let text_area = GetActiveTextArea();
if (!text_area) return;
let value = event.currentTarget.value;
let cursor = text_area.selectionStart;
let html_text = text_area.value;
text_area.value = html_text.slice(0, cursor) + value + html_text.slice(text_area.selectionEnd);
text_area.focus();
text_area.setSelectionRange(cursor + value.length, cursor + value.length);
}
//// Ruby dialog handlers
function SwitchRubySections(event) {
$('#ta-ruby-dialog-tabs .ta-menu-tab').removeClass('ta-active');
$(event.currentTarget).addClass('ta-active');
let value = $(event.currentTarget).data('value');
let selector = '#ta-ruby-dialog-styles-' + value;
$('#ta-ruby-dialog-style_sections > div').hide();
$(selector).show();
}
function PinRubyDialog() {
let $dialog_widget = TA.$ruby_dialog.closest('.ui-dialog');
let pos = $dialog_widget.offset();
if ($dialog_widget.css("position") === "absolute") {
pos.left -= $(window).scrollLeft();
pos.top -= $(window).scrollTop();
$dialog_widget.offset(pos).css({ position: "fixed" });
TA.$ruby_dialog.dialog("option", "resize", () => { $dialog_widget.css({ position: "fixed" }); });
TA.$pin_button.button("option", "icons", {primary: "ui-icon-pin-s"});
} else {
pos.left += $(window).scrollLeft();
pos.top += $(window).scrollTop();
$dialog_widget.offset(pos).css({ position: "absolute" });
TA.$ruby_dialog.dialog("option", "resize", () => { /* do nothing */ });
TA.$pin_button.button("option", "icons", {primary: "ui-icon-pin-w"});
}
}
function CopyRubyTag() {
let text_area = GetActiveTextArea();
if (!text_area) return;
let ruby_tag = GetRubyTag(text_area.value, text_area.selectionStart);
if (!ruby_tag) {
ShowErrorMessages(["No open ruby tag selected."]);
return;
}
let overall_style_dict = ParseDirectionStyles(ruby_tag.overall.style_dict);
$('#ta-ruby-dialog-styles-overall input').each((_i, input) => {
let style_name = input.dataset.name;
let style_value = overall_style_dict[style_name] || "";
input.value = style_value;
});
['top', 'bottom'].forEach((direction) => {
if (ruby_tag[direction].length) {
let style_dict = ruby_tag[direction][0].style_dict;
$(`#ta-ruby-dialog-styles-${direction} input`).each((_i, input) => {
let style_name = input.dataset.name;
let style_value = style_dict[style_name] || "";
input.value = style_value;
});
let segments = JSPLib.utility.getObjectAttributes(ruby_tag[direction], 'inner_html');
$(`#ta-ruby-${direction} textarea`).val(segments.join('\n'));
}
});
}
function ApplyRubyTag() {
let text_area = GetActiveTextArea(false);
if (!text_area) return;
SaveHTML(text_area);
let ruby_tag = GetRubyTag(text_area.value, text_area.selectionStart);
if (!ruby_tag && IsInsideHTMLTag(text_area.value, text_area.selectionStart)) {
JSPLib.notice.error(`Invalid selection range at cursor start... cannot create a ruby tag inside another tag.`);
return;
}
let top_segments = $('#ta-ruby-top textarea').val().split(/\r?\n/).filter((line) => line.trim() !== "");
let bottom_segments = $('#ta-ruby-bottom textarea').val().split(/\r?\n/).filter((line) => line.trim() !== "");
if (top_segments.length !== bottom_segments.length) {
JSPLib.notice.error(`The number of segments (lines) do not match: Top: ${top_segments.length} Bottom: ${bottom_segments.length}`);
return;
}
let [overall_add_styles, overall_invalid_styles] = GetCSSStyles(false, '#ta-ruby-dialog-styles-overall input');
let [top_add_styles, top_invalid_styles] = GetCSSStyles(false, '#ta-ruby-dialog-styles-top input');
let [bottom_add_styles, bottom_invalid_styles] = GetCSSStyles(false, '#ta-ruby-dialog-styles-bottom input');
let ruby_segments = [];
for (let i = 0; i < top_segments.length; i++) {
let top_segment = BuildHTMLTag('rt', {}, top_add_styles) + top_segments[i].replace(' ', '\u2002') + '';
let bottom_segment = BuildHTMLTag('span', {}, bottom_add_styles) + bottom_segments[i].replace(' ', '\u2002') + '';
ruby_segments.push(bottom_segment + top_segment);
}
let overall_segment = BuildHTMLTag('ruby', {}, overall_add_styles) + ruby_segments.join("") + '';
let select_start = (ruby_tag ? ruby_tag.overall.open_tag_start : text_area.selectionStart);
let final_text = text_area.value.slice(0, select_start);
final_text += overall_segment;
let select_end = final_text.length;
let slice_end = (ruby_tag ? ruby_tag.overall.close_tag_end : select_start);
final_text += text_area.value.slice(slice_end);
text_area.value = final_text;
text_area.focus();
text_area.setSelectionRange(select_start, select_end);
let style_errors = Object.entries(overall_invalid_styles).map((style_pair) => (`Overall - ${style_pair[0]}
=> "${style_pair[1]}"`));
style_errors = JSPLib.utility.concat(style_errors, Object.entries(top_invalid_styles).map((style_pair) => (`Top - ${style_pair[0]}
=> "${style_pair[1]}"`)));
style_errors = JSPLib.utility.concat(style_errors, Object.entries(bottom_invalid_styles).map((style_pair) => (`Bottom - ${style_pair[0]}
=> "${style_pair[1]}"`)));
if (style_errors.length) {
ShowStyleErrors(style_errors);
} else {
TA.$close_notice_link.click();
}
}
function LoadRubyStyles() {
OpenLoadDialog('ruby');
}
//// Load dialog handlers
function SaveSession() {
var name, key, isnew;
let panel = TA.active_panel;
let $dialog = TA.$load_dialog[panel];
let checked_sessions = $dialog.find('li').filter((i, entry) => $(entry).find('input').prop('checked'));
if (checked_sessions.length > 1) {
JSPLib.notice.error("Multiple sessions selected... select only 1 to edit, or none to create a new.");
return;
}
if (checked_sessions.length === 0) {
name = prompt("Enter a name for this session:");
if (!name) return;
name = JSPLib.utility.maxLengthString(name, 50);
key = JSPLib.utility.getUniqueID();
let session_list = JSPLib.storage.getStorageData('ta-load-session-' + panel, localStorage, []);
session_list.push({key, name});
JSPLib.storage.setStorageData('ta-load-session-' + panel, session_list, localStorage);
isnew = true;
} else {
({key, name} = checked_sessions.find('a')[0].dataset);
isnew = false;
}
let section_keys = LOAD_PANEL_KEYS[panel];
let save_inputs = {};
section_keys.forEach((key) => {
save_inputs = Object.assign(save_inputs, GetInputs(key));
});
JSPLib.storage.setStorageData('ta-session-' + key, save_inputs, localStorage);
if (isnew) {
let $load_item = $(RenderLoadItem({key, name}));
$load_item.find('a').on(PROGRAM_CLICK, LoadSessionInput);
let $list = TA.$load_dialog[panel].find('.ta-load-sessions ul');
if ($list.length === 0) {
$list = $('