// ==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 = `
Text shadow:
%SHADOWCSS%
%SHADOWGRID%
%SHADOWOPTIONS%
`; const RUBY_SUBSECTION = `
Ruby:
I.e. => bottom toptexttext
`; const EMBEDDED_SECTION = `
Change mode: ( toggle )

`; const CONTROLS_SECTION = `
Placement:
X:
 N/A
Y:
 N/A
Width:
 N/A
Height:
 N/A
Actions:
`; const CODES_SUBSECTION = `
HTML characters:
%HTMLCHARS%
Special characters:
%SPECIALCHARS%
Dash characters:
%DASHCHARS%
Space characters:
%SPACECHARS%
`; const NOTICE_INFO = ` Last noted: %LASTUPDATED%
 by %LASTUPDATER%
Total notes: %TOTALNOTES%   Embedded: %EMBEDDEDSTATUS%`; const MENU_OPTION = ` `; const RUBY_DIALOG = `
Styles:
%RUBYSTYLEOVERALL%
Text:
Add top segments to the Top section and bottom segments to the Bottom section. Separate each segment with a carriage return. Top/bottom segments can be separated by adding spaces to the lines. Note: The number of top segments and bottom segments must match.
Top
Bottom
`; const LOAD_DIALOG = `
Saved sessions (%LOADNAME%):
  • Click an item to load that session into the current set of inputs.
  • Click Delete to delete selected sessions.
  • Click Save to save the current inputs as a new session.
%LOADSAVED%
`; const NO_SESSIONS = '
There are no sessions saved.
'; const HTML_DOC_HEADER = 'Blah\n'; const HTML_DOC_FOOTER = '\n'; //Menu constants const TEXT_SHADOW_ATTRIBS = ['size', 'blur', 'color']; const CSS_OPTIONS = ['overwrite', 'initialize']; const HTML_CHARS = [{ display: '&', char: '&amp;', title: 'ampersand', }, { display: '<', char: '&lt;', title: 'less than', }, { display: '>', char: '&gt;', title: 'greater than', }, { display: '"', char: '&quot;', title: 'quotation', }, { display: ''', char: '&#x27;', title: 'apostrophe', }, { display: '`', char: '&#x60;', title: 'backtick', }]; const SPECIAL_CHARS = [{ char: '♥', title: 'black heart', }, { char: '\u2661', title: 'white heart', }, { char: '♦', title: 'black diamond', }, { char: '\u2662', title: 'white diamond', }, { char: '♠', title: 'black spade', }, { char: '\u2664', title: 'white spade', }, { char: '♣', title: 'black club', }, { char: '\u2667', title: 'white club', }, { char: '\u2669', title: 'quarter note', }, { char: '♪', title: 'eighth note', }, { char: '♫', title: 'beamed eighth notes', }, { char: '\u266C', title: 'beamed sixteenth notes', }, { char: '←', title: 'leftwards arrow', }, { char: '→', title: 'rightwards arrow', }, { char: '↓', title: 'downwards arrow', }, { char: '↑', title: 'upwards arrow', }, { char: '✓', title: 'check mark', }, { char: '✔', title: 'heavy check mark', }, { char: '★', title: 'black star', }, { char: '☆', title: 'white star', }, { char: '■', title: 'black square', }, { char: '□', title: 'white square', }, { char: '◆', title: 'black diamond', }, { char: '◇', title: 'white diamond', }, { char: '▲', title: 'black up triangle', }, { char: '△', title: 'white up triangle', }, { char: '▼', title: 'black down triangle', }, { char: '▽', title: 'white down triangle', }, { char: '❤', title: 'heavy black heart', }, { char: '💕', title: 'two hearts', }, { char: '•', title: 'bullet', }, { char: '●', title: 'black circle', }, { char: '○', title: 'white circle', }, { char: '◯', title: 'large circle', }, { char: '〇', title: 'ideographic number zero', }, { char: '💢', title: 'anger vein', }, { char: '…', title: 'horizontal ellipsis', }, { char: '\u22EE', title: 'vertical ellipsis', }, { char: '\u22EF', title: 'midline horizontalk ellipsis', }, { char: '\u22F0', title: 'up right diagonal ellipsis', }, { char: '\u22F1', title: 'down right diagonal ellipsis', }, { char: '¥', title: 'yen sign', }, { char: '*', title: 'fullwidth asterisk', }, { char: '※', title: 'reference mark', }, { char: '♂', title: 'Mars symbol', }, { char: '♀', title: 'Venus symbol', }, { char: '█', title: 'full block', }, { char: '░', title: 'light shade', }, { char: '\u223F', title: 'sine wave', }, { char: '〜', title: 'wave dash', }, { char: '〰', title: 'wavy dash', }, { char: '~', title: 'fullwidth tilde', }, { char: '\u299a', title: 'vertical zigzag', }, { char: '\u2307', title: 'wavy line', }]; const DASH_CHARS = [{ display: 'en', char: '–', title: 'en dash', }, { display: 'em', char: '—', title: 'em dash', }, { display: 'jp', char: 'ー', title: 'Katakana extension mark', }, { display: 'bar', char: '―', title: 'horizontal bar', }, { display: 'box', char: '─', title: 'box light horizontal', }]; const SPACE_CHARS = [{ display: 'en', char: '&ensp;', title: 'en space', }, { display: 'em', char: '&emsp;', title: 'em space', }, { display: 'thin', char: '&thinsp;', title: 'thin space' }, { display: 'nb', char: '&nbsp;', title: 'non-breaking space', }, { display: 'zero', char: '&ZeroWidthSpace;', title: 'zero-width space', }]; //UI constants const RUBY_DIALOG_SETTINGS = { title: "Ruby Editor", width: 750, height: 500, modal: false, resizable: false, autoOpen: false, position: {my: 'left top', at: 'left top'}, classes: { 'ui-dialog': 'ta-dialog', 'ui-dialog-titlebar-close': 'ta-dialog-close' }, buttons: [ { 'text': 'Load', 'click': LoadRubyStyles, }, { 'text': 'Clear', 'click'() { ClearInputs('#ta-ruby-dialog input, #ta-ruby-dialog textarea'); }, }, { 'text': 'Copy', 'click': CopyRubyTag, }, { 'text': 'Apply', 'click': ApplyRubyTag, }, { 'text': 'Undo', 'click': UndoAction, }, { 'text': 'Redo', 'click': RedoAction, }, { 'text': 'Close', 'click' () { $(this).dialog('close'); }, }, ] }; const LOAD_DIALOG_SETTINGS = { title: "Load Sessions", width: 500, height: 600, modal: false, draggable: true, resizable: false, autoOpen: false, position: {my: 'center', at: 'center'}, classes: { 'ui-dialog': 'ta-dialog', 'ui-dialog-titlebar-close': 'ta-dialog-close' }, buttons: [ { 'text': 'Save', 'click': SaveSession, }, { 'text': 'Rename', 'click': RenameSession, }, { 'text': 'Delete', 'click': DeleteSessions, }, { 'text': 'Close', 'click' () { $(this).dialog('close'); }, }, ] }; // Config constants const FAMILY_DICT = { margin: ['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom'], padding: ['padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom'], }; const STYLE_CONFIG = { 'font-family': { normalize (text) { return this._fixup(text, '"'); }, finalize (text) { return this._fixup(text, "'"); }, _fixup (text, char) { let family_list = text.split(','); let normalized_list = family_list.map((name) => { name = name.replace(/\s+/g, ' ').trim(); if (name.match(/^['"][^'"]+['"]$/)) { name = name.replace(/['"]/g, ""); } if (name.match(/ /)) { name = char + name + char; } return name; }); return normalized_list.join(', '); }, }, 'font-size': { normalize: NormalizeSize, }, direction_styles: { parse: ParseDirection, normalize (text) { return this._collapse(NormalizeSize(text).split(/\s+/)); }, finalize (text) { return this._collapse(text.split(/\s+/)); }, _collapse (size_list) { if (size_list.length === 0) return ""; if (size_list.every((size) => (size === size_list[0]))) return size_list[0]; size_list[3] = (size_list[3] === size_list[1] ? undefined : size_list[3]); size_list[2] = (size_list[2] === size_list[0] && size_list[3] === undefined ? undefined : size_list[2]); return size_list.filter((size) => (size !== undefined)).join(' '); }, }, color: { normalize: NormalizeColor, finalize: FinalizeColor, }, 'background-color': { normalize: NormalizeColor, finalize: FinalizeColor, label: 'letter-spacing: -1px;', }, rotate: { parse (_, value) { return (value !== "" ? ['transform', `rotate(${value})`] : ['transform', ""]); }, use_parse: true, } }; STYLE_CONFIG['line-height'] = STYLE_CONFIG['letter-spacing'] = STYLE_CONFIG['font-size']; STYLE_CONFIG['border-radius'] = JSPLib.utility.dataCopy(STYLE_CONFIG.direction_styles); ['margin', 'padding'].forEach((family) => { STYLE_CONFIG[family] = JSPLib.utility.dataCopy(STYLE_CONFIG.direction_styles); STYLE_CONFIG[family].family = FAMILY_DICT[family]; ['top', 'right', 'bottom', 'left'].forEach((direction) => { let style_name = family + '-' + direction; STYLE_CONFIG[style_name] = { copy: CopyDirection, family: FAMILY_DICT[family], }; }); }); const OPTION_CONFIG = { overwrite: { title: "Select to overwrite all styles; unselect to merge.", }, initialize: { title: "Select for blocks to be created with styles; unselect for blocks to be created empty.", }, }; // Regex constants const HTML_REGEX = /<(\/?)([a-z0-9]+)([^>]*)>/i; // Other constants const INPUT_SECTIONS = { 'main': '#ta-main-styles-subsection input', 'embedded': '#ta-embedded-style-subsection input', 'constructs': '#ta-text-shadow-attribs input', 'text-shadow-grid': '#ta-text-shadow-grid input', 'text-shadow-options': '#ta-text-shadow-options input', 'css-options': '#ta-menu-options input', 'ruby-overall-style': '#ta-ruby-dialog-styles-overall input', 'ruby-top-style': '#ta-ruby-dialog-styles-top input', 'ruby-bottom-style': '#ta-ruby-dialog-styles-bottom input', 'ruby-top-text': '#ta-ruby-top textarea', 'ruby-bottom-text': '#ta-ruby-bottom textarea', }; const LOAD_PANEL_KEYS = { main: ['main'], embedded: ['embedded'], constructs: ['constructs', 'text-shadow-grid'], ruby: ['ruby-overall-style', 'ruby-top-style', 'ruby-bottom-style'], }; const CLEANUP_LAST_NOTED = JSPLib.utility.one_hour; /****Functions****/ //Library functions JSPLib.utility.clamp = function (value, low, high) { return Math.max(low, Math.min(value, high)); }; // Helper functions function ShowErrorMessages(error_messages, header = 'Error') { let header_name = (error_messages.length === 1 ? header : header + 's'); let error_html = error_messages.map((message) => ('* ' + message)).join('
'); JSPLib.notice.error(`
${header_name}:
${error_html}
`, true); } function ShowStyleErrors(style_errors) { ShowErrorMessages(style_errors, 'Invalid style'); } // Render functions function RenderSideMenu() { let shadow_section = (TA.user_settings.text_shadow_enabled ? JSPLib.utility.regexReplace(TEXT_SHADOW_SUBSECTION, { SHADOWCSS: RenderSectionTextInputs('text-shadow', TEXT_SHADOW_ATTRIBS, {}), SHADOWGRID: RenderTextShadowGrid(), SHADOWOPTIONS: RenderSectionCheckboxes('text-shadow', ['append'], {}) }) : ""); let ruby_section = (TA.user_settings.ruby_enabled ? RUBY_SUBSECTION : ""); let constructs_section = (TA.user_settings.text_shadow_enabled || TA.user_settings.ruby_enabled ? shadow_section + ruby_section + '
' : ""); let embedded_section = (TA.user_settings.embedded_enabled ? JSPLib.utility.regexReplace(EMBEDDED_SECTION, { EMBEDDEDCSS: RenderSectionTextInputs('embedded-style', TA.user_settings.available_embedded_styles, STYLE_CONFIG), }) : ""); let codes_section = (TA.user_settings.codes_enabled ? JSPLib.utility.regexReplace(CODES_SUBSECTION, { HTMLCHARS: RenderCharButtons(HTML_CHARS), SPECIALCHARS: RenderCharButtons(SPECIAL_CHARS), DASHCHARS: RenderCharButtons(DASH_CHARS), SPACECHARS: RenderCharButtons(SPACE_CHARS), }) : ""); let html = JSPLib.utility.regexReplace(SIDE_MENU, { BLOCKHTML: RenderHTMLBlockButtons(), BLOCKCSS: RenderSectionTextInputs('block-style', TA.user_settings.available_css_styles, STYLE_CONFIG), CONSTRUCTSTAB: constructs_section, EMBEDDEDTAB: embedded_section, CONTROLSTAB: CONTROLS_SECTION, CODESTAB: codes_section, CSSOPTIONS: RenderSectionCheckboxes('css-style', CSS_OPTIONS, OPTION_CONFIG), }); return html; } function RenderRubyDialog() { let available_inner_styles = JSPLib.utility.arrayIntersection(INNER_RUBY_STYLES, TA.user_settings.available_ruby_styles); return JSPLib.utility.regexReplace(RUBY_DIALOG, { RUBYSTYLEOVERALL: RenderSectionTextInputs('ruby-overall-style', TA.user_settings.available_ruby_styles, STYLE_CONFIG), RUBYSTYLETOP: RenderSectionTextInputs('ruby-top-style', available_inner_styles, STYLE_CONFIG), RUBYSTYLEBOTTOM: RenderSectionTextInputs('ruby-bottom-style', available_inner_styles, STYLE_CONFIG), }); } function RenderLoadDialog(panel) { let sessions = JSPLib.storage.getStorageData('ta-load-session-' + panel, localStorage, []); return JSPLib.utility.regexReplace(LOAD_DIALOG, { LOADNAME: panel, LOADSAVED: RenderLoadSessions(panel, sessions), }); } function RenderLoadSessions(panel, sessions) { let html = ""; let updated_list = []; sessions.forEach((item) => { if (item.key) { html += RenderLoadItem(item); updated_list.push(item); } else { JSPLib.debug.debugerror("Malformed item found:", item); } }); if (updated_list.length !== sessions.length) { JSPLib.storage.setStorageData('ta-load-session-' + panel, updated_list, localStorage); } return (html === "" ? NO_SESSIONS : ``); } function RenderLoadItem(item) { let checkbox_key = 'ta-delete-' + item.key; let escaped_name = JSPLib.utility.HTMLEscape(item.name); return `
  • ${escaped_name}
  • `; } function RenderHTMLBlockButtons() { let block_html = ""; HTML_TAGS.forEach((tag) => { if (!TA.user_settings.available_html_tags.includes(tag)) return; let button_class = (HTML_STYLE_TAGS.includes(tag) ? 'ta-html-style-tag' : 'ta-html-only-tag'); block_html += ``; }); return block_html; } function RenderSectionTextInputs(section_class, section_names, config) { let html = ""; section_names.forEach((name) => { let display_name = JSPLib.utility.displayCase(name); let input_name = section_class + '-' + JSPLib.utility.kebabCase(name); let label_style = config[name]?.label || ""; let value = TA.save_data[input_name] || ""; html += `
    `; }); return html; } function RenderSectionCheckboxes(section_class, section_names, config) { let html = ""; section_names.forEach((name) => { let display_name = JSPLib.utility.displayCase(name); let input_name = section_class + '-' + JSPLib.utility.kebabCase(name); let title = (config[name]?.title ? `title="${config[name].title}"` : ""); let checked = (TA.save_data[input_name] !== true ? "" : 'checked'); html += `
    `; }); return html; } function RenderTextShadowGrid() { let grid_html = ""; let right_em = 9; let col_val = -1; ['left', 'center', 'right'].forEach((colname) => { let top_em = 1; let row_val = -1; ['top', 'middle', 'bottom'].forEach((rowname) => { if (colname === 'center' && rowname === 'middle') { grid_html += `
    `; } else { let input_name = 'shadow-grid-' + rowname + '-' + colname; let classname = (row_val === 0 || col_val === 0 ? 'ta-grid-side' : 'ta-grid-corner'); TA.shadow_grid[input_name] = TA.save_data[input_name] !== false; let checked = (TA.shadow_grid[input_name] ? "checked" : ""); grid_html += ``; } top_em += 4; row_val += 1; }); right_em -= 4; col_val += 1; }); return `
    ${grid_html}
    `; } function RenderCharButtons(char_list) { let html = ""; char_list.forEach((item) => { let display = item.display || item["char"]; html += ``; }); return html; } function RenderHTMLError(iteration, message, input_html) { let number = JSPLib.utility.padNumber(iteration + 1, 2); if (!('firstColumn' in message)) { return `
  • ${number}. ${message.message}
  • `; } var highlight_html, row, column; let line = input_html.split('\n')[message.lastLine - 2]; if (line !== undefined) { highlight_html = '' + 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 `
  • ${number}. ${message.message}
  • `; } function RenderCSSError(iteration, error) { let highlight_html = JSPLib.utility.HTMLEscape(error.excerpt); let message_html = JSPLib.utility.HTMLEscape(error.message); let letter = String.fromCharCode(65 + iteration); return `
  • ${letter}. ${message_html}
  • `; } function RenderUserLink(user) { let user_name = JSPLib.utility.maxLengthString(user.name); return ` `; } //Network functions function QueryNoteVersions(search_options, query_options) { let send_options = {search: {post_id: TA.post_id}, limit: 1}; Object.assign(send_options.search, search_options); Object.assign(send_options, query_options); return JSPLib.danbooru.submitRequest('note_versions', send_options); } function QueryNewNotations() { QueryNoteVersions({id_gt: TA.last_id, updater_id_not_eq: TA.user_id}, {only: 'id,updated_at'}).then((data) => { if (data.length > 0) { JSPLib.debug.debuglog("New note record:", data); alert("New noter detected: " + JSPLib.utility.timeAgo(data[0].updated_at)); TA.noter_detected = true; } else { JSPLib.debug.debuglog("No new noter detected."); } }); } function QueryLastNotation() { let query_options = {only: 'id,updated_at,updater[id,name,level,level_string]'}; if (TA.user_settings.last_noter_cache_time > 0) { query_options.expires_in = TA.user_settings.last_noter_cache_time + 'min'; } let search_options = (TA.user_settings.filter_last_noter_enabled ? {updater_id_not_eq: TA.user_id} : {}); QueryNoteVersions(search_options, query_options).then((data) => { JSPLib.debug.debuglog("Last note record:", data); TA.last_noter_queried = true; let timeago_timestamp = (data.length ? JSPLib.utility.timeAgo(data[0].updated_at) : 'N/A'); let last_updater = (data.length ? RenderUserLink(data[0].updater) : 'N/A'); let total_notes = $('#notes > article').length; let [embedded_status, embedded_color] = (TA.has_embedded ? ['Enabled', 'green'] : ['Disabled', 'red']); let html = JSPLib.utility.regexReplace(NOTICE_INFO, { LASTUPDATED: timeago_timestamp, LASTUPDATER: last_updater, TOTALNOTES: total_notes, EMBEDDEDSTATUS: embedded_status, EMBEDDEDCOLOR: embedded_color, }); TA.$text_box.html(html); ToggleSideMenu(true, false); TA.last_id = data[0]?.id || TA.last_id; }); } async function ValidateHTML(input_html) { var data, resp; let send_html = HTML_DOC_HEADER + input_html + HTML_DOC_FOOTER; try { //Replace this with a JSPLib.network.post version resp = await GM.xmlHttpRequest({ method: 'POST', url: 'https://validator.nu?out=json', headers: {'Content-Type': "text/html; charset=UTF-8"}, data: send_html, }); } catch(e) { JSPLib.notice.error("Error querying validation server 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 = ''; 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('
    ')); return false; } attribs.color = FinalizeColor(attribs.color); let grid_points = $('#ta-text-shadow-grid input').filter((i, entry) => entry.checked).map((i, entry) => entry.value).toArray().map(JSON.parse); let text_shadows = grid_points.map((grid_point) => { let horizontal = (grid_point[0] === 0 ? '0' : "") || ((grid_point[0] === -1 ? '-' : "") + attribs.size); let vertical = (grid_point[1] === 0 ? '0' : "") || ((grid_point[1] === -1 ? '-' : "") + attribs.size); let text_shadow = horizontal + ' ' + vertical; text_shadow += (attribs.blur !== "" ? ' ' + attribs.blur : ""); text_shadow += (attribs.color !== "" ? ' ' + attribs.color : ""); return text_shadow; }); return (initial_shadow ? initial_shadow + ', ' : "") + text_shadows.join(', '); } function TokenizeTextShadow(shadow) { let shadow_tokens = shadow.split(/\s+/); if (shadow_tokens.length < 2) return; if (!ValidateSize(shadow_tokens[0]) || !ValidateSize(shadow_tokens[1])) return; let size = (shadow_tokens[0].match(/^-?(?:0\.|[1-9]).*/) || shadow_tokens[1].match(/^-?(?:0\.|[1-9]).*/) || [])[0]; if (!size) return; size = (size[0] === '-' ? size.slice(1) : size); let shadow_style = {size}; if (shadow_tokens.length === 3) { if (ValidateSize(shadow_tokens[2])) { shadow_style.blur = shadow_tokens[2]; } else if (ValidateColor(shadow_tokens[2])) { shadow_style.color = shadow_tokens[2]; } } else if (shadow_tokens.length === 4) { if (ValidateSize(shadow_tokens[2])) { shadow_style.blur = shadow_tokens[2]; } if (ValidateColor(shadow_tokens[3])) { shadow_style.color = shadow_tokens[3]; } } return shadow_style; } function ParseTextShadows(style_dict) { if (!style_dict['text-shadow']) return; let text_shadows = style_dict['text-shadow'].split(',').map((str) => str.trim()); //The first shadow is used for style parsing let shadow_style = TokenizeTextShadow(text_shadows[0]); if (!shadow_style) return; ['left', 'center', 'right'].forEach((colname) => { ['top', 'middle', 'bottom'].forEach((rowname) => { let input_name = 'shadow-grid-' + rowname + '-' + colname; TA.shadow_grid[input_name] = false; }); }); text_shadows.forEach((shadow) => { let style = TokenizeTextShadow(shadow); if (!style) return; //Break out of the loop when a new style is detected if (style.blur !== shadow_style.blur || style.color !== shadow_style.color) return false; let match = shadow.match(/^\s*(-)?(\d)\S*\s+(-)?(\d)/); if (!match) return null; let colname = (match[1] ? 'left' : (match[2] === '0' ? 'center' : 'right')); let rowname = (match[3] ? 'top' : (match[4] === '0' ? 'middle' : 'bottom')); let input_name = 'shadow-grid-' + rowname + '-' + colname; TA.shadow_grid[input_name] = true; }); JSPLib.debug.debuglog('ParseTextShadows', {shadow_style, shadow_grid: TA.shadow_grid}); return shadow_style; } // Dialogs function OpenLoadDialog(panel) { if (!TA.$load_dialog[panel]) { let $dialog = $(RenderLoadDialog(panel)); $dialog.find('.ta-load-session-item').on(PROGRAM_CLICK, LoadSessionInput); $dialog.find('.ta-load-saved-controls a').on(PROGRAM_CLICK, LoadControls); $dialog.dialog(LOAD_DIALOG_SETTINGS); TA.$load_dialog[panel] = $dialog; } TA.active_panel = panel; TA.$load_dialog[panel].dialog('open'); } function OpenRubyDialog() { if (!TA.$ruby_dialog) { TA.$ruby_dialog = $(RenderRubyDialog()); TA.$ruby_dialog.find('#ta-ruby-dialog-tabs > .ta-menu-tab').on(PROGRAM_CLICK, SwitchRubySections); TA.$ruby_dialog.dialog(RUBY_DIALOG_SETTINGS); TA.$pin_button = $("