Styx / Fanfics Extender

// ==UserScript==
// @name         Fanfics Extender
// @namespace    https://fanfics.me/user159153
// @version      0.1.13
// @description  Useful features for fanfics.me
// @author       Styx
// @copyright    2017, Styx (https://fanfics.me/user159153)
// @license      MIT; https://opensource.org/licenses/MIT
// @homepageURL  https://fanfics.me/index.php?section=blogs&search=%23ffme
// @supportURL   https://fanfics.me/index.php?section=blogs&search=%23ffme
// @updateURL    https://openuserjs.org/meta/Styx/Fanfics_Extender.meta.js
// @include      https://fanfics.me/*
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @connect      coub.com
// @connect      youtube.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.4/js/select2.min.js
// ==/UserScript==

/**
  Copyright 2017 Styx (https://fanfics.me/user159153)

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
  to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * RELEASE NOTES:
 *
 * 0.1.13
 *   + Стилизация диалогов.
 *   + Добавлены кнопки разметки к полю нового сообщения в диалогах; thx "хочется жить" (https://fanfics.me/user219670)
 *   * При цитировании после блока цитаты теперь вставляется перенос строки вместо пробела; thx Fredo (https://fanfics.me/user287969)
 *
 * 0.1.12
 *   * Цитирование теперь поддерживает картинки.
 *   * Если цитировать нечего, то будет вставлено просто обращение к автору (согласно настройкам обращения).
 *   + Добавлен расширенный редактор (бета): в общей ленте, в личном профиле, на странице собственного сообщения.
 *
 * 0.1.11
 *   * Исправление работы цитирования после обновления сайта.
 *   * Исправлено отображение кнопки "В архив" при просмотре собственного одиночного сообщения, теперь она отображается в выпадающем меню.
 *   * Теперь кнопка "В архив" располагается после кнопки "Изменить" (в выпадающем меню); thx финикийский_торговец (https://fanfics.me/user159440)
 *   * Исправлена работа кнопки "В архив" (региональная несовместимость с archive.li); thx "Fluxius Secundus" (https://fanfics.me/user63459)
 *   * Всплывающие окна теперь открываются без анимации и мгновенно; thx Desmоnd (https://fanfics.me/user266814)
 *   * Кнопка "Ответить" при просмотре сообщения больше не показывается, если комментирование запрещено; thx Fredo (https://fanfics.me/user287969)
 *   * Экстендер теперь не будет пытаться запуститься второй раз, если одна копия уже запущена; thx Fredo (https://fanfics.me/user287969), "Ал Ластор" (https://fanfics.me/user49176)
 *   + Кнопки "Ссылка на пользователя" и "Ссылка на фанфик" теперь умеют просто обрамлять выделенный текст; thx Desmоnd (https://fanfics.me/user266814)
 *   + Добавлена поддержка сохранения разметки и ссылок при цитировании.
 *
 * 0.1.10
 *   - Убрана поддержка старого дизайна.
 *   - Окончательно убрана поддержка HTTP.
 *
 * 0.1.9
 *   + Добавлена поддержка HTTPS; thx ReFeRy (https://fanfics.me/user43)
 *   * Исправлена вставка ссылки на фанфик
 *   - Убрана поддержка HTTP
 *   - Убрана поддержка Pichome
 *
 * 0.1.8
 *   * Исправлено отображение длинных имен в компактном окне мимимишек; thx lrkis (https://fanfics.me/user234818)
 *   * Исправлено добавление кнопок к комментариям к фику; thx Desmоnd (https://fanfics.me/user266814)
 *   * На страницу просмотра одиночного сообщения возвращена кнопка "Ответить" (новый дизайн)
 *   * Исправлена ошибка, из-за которой иногда ломалось отображение Youtube/Coub контента
 *   * Карта разделов/глав на странице чтения фика больше не будет показываться, если она не нужна
 *   + В угадайку добавлен живой поиск пользователей
 *   + К комментариям в блогах добавлены кнопки цитирования
 *   + К полям добавления комментария добавлена кнопка вставки Привет-ведьмы Фанфикса
 *
 * 0.1.7
 *   + Добавлена карта разделов/глав на странице чтения фанфика целиком; thx "Жопожуй Конидзэ" (https://fanfics.me/user283586)
 *   * Исправлена ошибка, из-за которой скрипт не работал на страницах чтения фанфика/ФвФ
 *   * Небольшая оптимизация работы скрипта на страницах чтения фанфика/ФвФ
 *
 * 0.1.6
 *   * При редактировании главы (глав) добавлена возможность импорта текста из Google Docs; thx Desmоnd (https://fanfics.me/user266814)
 *   * Добавлена опция предварительной очистки текста главы (глав) при импорте текста из Google Docs/MS Word; thx Desmоnd (https://fanfics.me/user266814)
 *
 * 0.1.5
 *   * Исправлено добавление прямых ссылок на комментарии (новый дизайн)
 *
 * 0.1.4
 *   * Исправлена работоспособность на обоих дизайнах
 *   * Кнопка "В архив" по возможности помещается в выпадающее меню сообщения (новый дизайн)
 *   * Кнопка "Отправить" (создание поста) заменена на иконку (новый дизайн)
 *   * Убрана кнопка "Редактор" (новый дизайн)
 *   + Добавлена опция принудительного показа полного выпадающего меню (новый дизайн); thx Desmоnd (https://fanfics.me/user266814)
 *   + Добавлена опция принудительного перенаправления на старый/новый дизайн; thx Wave (https://fanfics.me/user185121)
 *   + Добавлена опция исправления ссылок, чтобы они вели на старый/новый дизайн; thx Wave (https://fanfics.me/user185121)
 *   + Добавлены прямые ссылки на комментарии (новый дизайн)
 *
 * 0.1.3
 *   * Исправлена функция автоматического переключения видимости новых постов; thx BufferOverflow (https://fanfics.me/user100460)
 *   * Исправлено создание ссылок на комментарии в блогах
 *   * Кнопка "В архив" заменена на иконку; thx "Fluxius Secundus" (https://fanfics.me/user63459)
 *   * Исправлено ограничение размеров картинок; thx Dillaria (https://fanfics.me/user248353)
 *   * Добавлена кнопка "Ответить" при просмотре одиночного сообщения
 *   * К полю добавления/редактирования заметки добавлены некоторые кнопки форматирования; tnx "Ал Ластор" (https://fanfics.me/user49176)
 *
 * 0.1.2
 *   * Неканоничные ссылки на uploads.ru теперь не будут пытаться загрузиться как картинки; thx "Fluxius Secundus" (https://fanfics.me/user63459)
 *   + Добавлены опции раскрытия ссылок на Youtube/Coub при просмотре одиночного сообщения
 *   + Добавлен фикс для максимальной ширины текста заявки в списке заявок; thx "Ал Ластор" (https://fanfics.me/user49176)
 *   + Добавлена опция подсветки ссылок на заявки
 *   + Добавлена интеграция с Pichome Extender
 *
 * 0.1.1
 *   * Исправление высоты сообщений со свернутыми спойлерами; thx BufferOverflow (https://fanfics.me/user100460)
 *   * Исправлено раскрытие ссылок на картинки с расширением в верхнем регистре; thx Wave (https://fanfics.me/user185121)
 *   * Теперь раскрытие ссылок на картинки/видео не теряет текст самих ссылок; thx "Fluxius Secundus" (https://fanfics.me/user63459)
 *   * Безопасная вставка ссылок на пользователей и фанфики
 *   + Добавлена опция автоматического переключения "В общую ленту"/"В личный блог"; thx Desmоnd (https://fanfics.me/user266814)
 *   + Добавлено исправление для ссылок в блогах для Firefox; thx "Ал Ластор" (https://fanfics.me/user49176)
 *   + Добавлена возможность архивировать посты на archive.li; thx "Fluxius Secundus" (https://fanfics.me/user63459)
 *   + В блок меню добавлена ссылка на пост "Вопросы, жалобы и предложения"
 *   + Добавлена опция смены положения кнопки персонального баннера; thx ArtChaos (https://fanfics.me/user123053)
 *   + Добавлена поддержка зачеркнутого текста; thx "Жопожуй Конидзэ" (https://fanfics.me/user283586)
 *   + Добавлена вкладка "Инфо" в настройках
 *
 * 0.1.0
 *   * Исправлена ошибка, препятствующая добавлению кнопок к полям редактирования
 *   * Исправлены подсветка и вставка контента после редактирования постов/комментариев
 *   * Исправлена высота свёрнутого спойлера в комментариях к фикам
 *   + Настройки разделены по группам
 *   + Добавлено определение размеров вставляемых Youtube/Coub видео; thx Dillaria (https://fanfics.me/user248353)
 *   + Добавлена опция, позволяющая включить анонимный просмотр Youtube видео
 *   + Добавлена опция автоматической прокрутки к началу главы при навигации по фанфику
 *   + Клик на ссылках в спойлере теперь не сворачивает сам спойлер; thx pskovoroda (https://fanfics.me/user181551)
 *   + Выделение текста в спойлере также не сворачивает сам спойлер
 *   + Добавлено исправление глюка с прыжком поля редактирования поста вверх/вниз при наведении мыши (https://fanfics.me/message98129?comment_id=2312236)
 *   + Более умные кнопки разметки
 *   + Добавлена кнопка «Очистить форматирование»
 *   + Добавлена кнопка «Спойлер» к полям комментариев к фику; thx "Читатель 1111" (https://fanfics.me/user249227)
 *
 * 0.0.9
 *   * Исправлен "хмурый" фон подсвеченных тэгов; thx Dillaria (https://fanfics.me/user248353)
 *   * Исправлена иконка кнопки "Цитировать"; thx Wave (https://fanfics.me/user185121)
 *   * Исправлена подсветка внешних ссылок на поиск и на пользователей; thx "Ал Ластор" (https://fanfics.me/user49176)
 *   + Добавлены опции раскрытия ссылок на YouTube и Coub; возможен показ только во всплывающем окне
 *   + Добавлена опция включения навигация клавиатурой по главам фика
 *
 * 0.0.8
 *   * Исправлена изначальная высота "компактного" окна мимимишек; thx ReFeRy (https://fanfics.me/user43)
 *   * Исправлено позиционирование подсвеченных тэгов и ссылок
 *   * Исправлена добавление лишних кнопок настроек FFME на странице "Настройки"
 *   * Тэги теперь также парсятся в постах на странице "Обсуждения"
 *
 * 0.0.7
 *   * При включенной опции "Показывать оригинальный размер картинки по клику" клик по картинке в спойлере теперь не сворачивает сам спойлер
 *   * Теперь также раскрываются ссылки на картинки с расширением .jpeg
 *   * Улучшены иконки кнопок разметки; thx Chaucer (https://fanfics.me/user183901)
 *   * Подсветку тэгов теперь можно отключить; thx Chaucer (https://fanfics.me/user183901)
 *   * Верхние границы максимальных размеров картинки увеличены до 750; thx "читатель 1111" (https://fanfics.me/user249227)
 *   * Все функции теперь работают на страницах "Блоги -> Подписка" и "Обсуждения"; thx Marlagram (https://fanfics.me/user64105)
 *   + Добавлена опция парсинга тэгов в комментариях
 *   + Добавлены опции подсветки ссылок, ссылок на фики и ссылок на пользователей
 *   + Добавлена опция компактного вида окна мимимишек
 *   + Добавлена опция ограничения размеров картинок в спойлерах
 *   + Добавлены ссылки на комментарии в блогах; thx Desmond (https://fanfics.me/user266814)
 *
 * 0.0.6
 *   * Восстановлена совместимость с менеджерами скриптов, отличными от Tampermonkey
 *   * Исправлена работа функции "Однострочное обращение по нику"
 *   * Для поиска пользователей используется более подходящий API; thx ReFeRy (https://fanfics.me/user43)
 *   * Всплывающие окна вставки ссылки, пользователя и фанфика теперь закрываются по клику вне окна; thx Fаker (https://fanfics.me/user81624)
 *
 * 0.0.5
 *   + Добавлено закрытие всплывающего окна с картинкой по клику; thx Faker (https://fanfics.me/user81624)
 *   + Добавлено выделение тэгов
 *   + Добавлены кнопки вставки ссылок на пользователей и фанфики
 *
 * 0.0.4
 *   * Исправлено добавление кнопок разметки после добавления комментария; thx Wave (https://fanfics.me/user185121)
 *   + Кнопки разметки обзавелись иконками
 *   + Добавлена кнопка вставки ссылок
 *   + Добавлены опции раскрытия ссылок на картинки (отдельно для статичных и анимированных картинок)
 *
 * 0.0.3
 *   * Ограничение размеров картинок теперь работает в разделе "Мои обсуждения"
 *   + Добавлена кнопка "Цитата" к полям добавления и редактирования комментариев (фик); thx Wave (https://fanfics.me/user185121)
 *   - Удалена опция "Ограничивать размеры картинок в сообщениях -> Отображать оригинал по клику"
 *   + Добавлена опция "Показывать оригинальный размер картинки по клику"; thx Wave (https://fanfics.me/user185121)
 *   + Добавлена опция "Однострочное обращение по нику"
 *
 * 0.0.2
 *   * Кнопки разметки также добавлены к полям редактирования
 *   + Добавлена опция ограничения размеров картинок в блогах
 *   + Добавлена опция сворачивания скрытого спойлера
 *
 * 0.0.1
 *   + Release
 */

(function (win, $) {
  'use strict';
  if (typeof $ === 'undefined') return;

  const VERSION = '0.1.13';
  const EDITOR_VERSION = '0.1';
  // const EDITOR_ENDPOINT = 'https://ffmelibs.local/';
  const EDITOR_ENDPOINT = 'https://extenderlibs.dostatic.com/ffme/';

  const __current__ = win.eval(`window.__fanfics_extender__`);
  if (__current__ != null) {
    win.console.error(`Fanfics Extender ERROR!\nSecond instance [${VERSION}] has tried to run, but [${__current__}] is already running!\nCheck your scripts in Tampermonkey settings.`);
    return;
  }
  win.eval(`window.__fanfics_extender__ = '${VERSION}';`);

  const isAlone = /^\/message\d+/.test(window.location.pathname);
  const isAloneOwn = isAlone && document.querySelector('.Message.alone a[onclick^="delitem("]') != null;

  const isFicWrite = win.location.pathname === '/fic_write';
  const isFicRead = /^\/read2?\.php/.test(win.location.pathname);
  const isFicHead = /^\/fic\d+/.test(win.location.pathname);
  const isFtFRead = /^\/read2\.php/.test(win.location.pathname);
  const isFicReadAll = isFicRead && !isFtFRead && !/chapter=/.test(win.location.search);
  const isBlogs = /^\/blogs$/.test(win.location.pathname);
  const isProfile = /^\/user\d+$/.test(win.location.pathname);
  let isOwnProfile = false;

  win.console.info(`Fanfics Extender [${VERSION}]`);

  const escapeMarkup = $.fn.select2.defaults.defaults.escapeMarkup;
  const LIVE_SEARCH_DELAY = 500;
  const isMac = /(Macintosh|MacIntel|MacPPC)/.test(window.navigator.platform);
  const origin = `https://${window.location.hostname}`;
  const https = window.location.protocol.indexOf('https') === 0 ? 's' : '';

  const CONF_IMAGE_TETHER = 'image_tether'
      , CONF_IMAGE_TETHER_WIDTH = 'image_tether_width'
      , CONF_IMAGE_TETHER_HEIGHT = 'image_tether_height'
      , CONF_IMAGE_TETHER_SPOILER = 'image_tether_spoiler'
      , CONF_IMAGE_ZOOM_BY_CLICK = 'image_zoom_by_click'
      , CONF_SPOILER_COLLAPSE_HIDDEN = 'spoiler_collapse_hidden'
      , CONF_ONELINE_ADDRESS = 'oneline_address'
      , CONF_IMAGE_LINKS_STATIC = 'image_links_static'
      , CONF_IMAGE_LINKS_ANIMATED = 'image_links_animated'
      , CONF_LINKS_YOUTUBE = 'links_youtube'
      , CONF_LINKS_YOUTUBE_POPUP = 'links_youtube_p'
      , CONF_LINKS_YOUTUBE_ALONE = 'links_youtube_a'
      , CONF_LINKS_YOUTUBE_ANON = 'links_youtube_anon'
      , CONF_LINKS_COUB = 'links_coub'
      , CONF_LINKS_COUB_POPUP = 'links_coub_p'
      , CONF_LINKS_COUB_ALONE = 'links_coub_a'
      , CONF_TAGS_HIGHLIGHT = 'tags_hl'
      , CONF_TAGS_PARSE = 'tags_cp'
      , CONF_LINKS_HIGHLIGHT = 'links_hl'
      , CONF_LINKS_FF_HIGHLIGHT = 'links_ff_hl'
      , CONF_LINKS_U_HIGHLIGHT = 'links_u_hl'
      , CONF_LINKS_R_HIGHLIGHT = 'links_r_hl'
      , CONF_FIX_LIKES_MODAL = 'fix_likes_modal'
      , CONF_KBD_NAV = 'kbd_nav'
      , CONF_NAV_SCROLL = 'nav_scroll'
      , CONF_SCROLL_NEXT = 'scroll_next'
      , CONF_BLOG_AUTO_DST = 'blog_auto_dst'
      , CONF_BLOG_ARCHIVE_LINKS = 'blog_arj'
      , CONF_MOVE_BANNER = 'move_banner'
      , CONF_FULL_MENU = 'nd_full_menu'
      , CONF_RICH_EDITOR = 'rich_editor'
  ;

  const DEFAULTS = {};
  DEFAULTS[CONF_IMAGE_TETHER] = false;
  DEFAULTS[CONF_IMAGE_TETHER_WIDTH] = 200;
  DEFAULTS[CONF_IMAGE_TETHER_HEIGHT] = 200;
  DEFAULTS[CONF_IMAGE_TETHER_SPOILER] = false;
  DEFAULTS[CONF_IMAGE_ZOOM_BY_CLICK] = true;
  DEFAULTS[CONF_SPOILER_COLLAPSE_HIDDEN] = false;
  DEFAULTS[CONF_ONELINE_ADDRESS] = false;
  DEFAULTS[CONF_IMAGE_LINKS_STATIC] = false;
  DEFAULTS[CONF_IMAGE_LINKS_ANIMATED] = false;
  DEFAULTS[CONF_TAGS_PARSE] = true;
  DEFAULTS[CONF_TAGS_HIGHLIGHT] = true;
  DEFAULTS[CONF_LINKS_HIGHLIGHT] = true;
  DEFAULTS[CONF_LINKS_FF_HIGHLIGHT] = true;
  DEFAULTS[CONF_LINKS_U_HIGHLIGHT] = true;
  DEFAULTS[CONF_LINKS_R_HIGHLIGHT] = true;
  DEFAULTS[CONF_FIX_LIKES_MODAL] = true;
  DEFAULTS[CONF_LINKS_YOUTUBE] = false;
  DEFAULTS[CONF_LINKS_YOUTUBE_POPUP] = false;
  DEFAULTS[CONF_LINKS_YOUTUBE_ALONE] = true;
  DEFAULTS[CONF_LINKS_YOUTUBE_ANON] = true;
  DEFAULTS[CONF_LINKS_COUB] = false;
  DEFAULTS[CONF_LINKS_COUB_POPUP] = false;
  DEFAULTS[CONF_LINKS_COUB_ALONE] = true;
  DEFAULTS[CONF_KBD_NAV] = false;
  DEFAULTS[CONF_NAV_SCROLL] = false;
  DEFAULTS[CONF_SCROLL_NEXT] = false;
  DEFAULTS[CONF_BLOG_AUTO_DST] = false;
  DEFAULTS[CONF_BLOG_ARCHIVE_LINKS] = false;
  DEFAULTS[CONF_MOVE_BANNER] = false;
  DEFAULTS[CONF_FULL_MENU] = false;
  DEFAULTS[CONF_RICH_EDITOR] = false;


  let SETTINGS = {};
  let CACHE = {};

  let UID = null;
  let userName = null;

  win.eval('window.beforeEdit = {}');
  win.eval('window.ffmeCall = function (m) { window.postMessage(m, window.location.protocol+"//"+window.location.hostname); }');
  win.eval('window.preventObserve = false');
  win.eval('window.overwriteA = false');

  const CSS = `
    .MessageText a,
    .RecommendText a,
    .MyCommentsItemRight a:not(.MyCommentsItemShow)
    { display: inline-block; }
    
    .FicTbl_sammary { word-break: break-word; }
    
    .clearfix:after {
      content: "";
      display: table;
      clear: both;
    }
    
    input.input_modal {
      display: block;
      padding: 4px;
      width: 100%;
      box-sizing: border-box;
    }
    .select_modal {
      width: 100%;
      box-sizing: border-box;
    }  
    
    button.ffe-blog-text-attr {
      vertical-align: top;
      margin: 0 0 0 4px;
      float: left;
      padding: 0;
      width: 2em;
      height: 22px;
      background: #efefdb;
      display: inline-block;
      font-size: 12px;
      border-radius: 2px;
      cursor: pointer;
      border: 1px solid #92927e;
    }
    
    button.ffe-blog-text-attr:hover {
      background: #d8d8c4;
    }
    
    button.ffe-blog-text-attr:active {
      position: relative;
      top: 2px;
    }
    
    button.ffe-blog-text-attr i.fa {
      line-height: 19px;
      font-size: 12px;
      color: #222;
      vertical-align: middle;
    }
    
    .MessageCommentNewInstr button.ffe-blog-text-attr.ffe-bold {
      margin-left: 10px;
    }

    .MessageNewInstr input[type="submit"], .MessageCommentNewInstr input[type="submit"] {
      padding: 0 6px;
      height: 22px;
      line-height: 20px;
    }
    .MessageNewInfoHead {
      padding-right: 6px;
    }
    .MessageNewInfoHead button.ffe-blog-text-attr {
      margin: -2px 4px -2px 0;
    }
    
    .MessageNewInfoHead button.ffe-blog-text-attr:last-of-type {
      margin-right: 8px;
    }
    
    .CommentEditManagement button.ffe-blog-text-attr,
    #comment_button_span button.ffe-blog-text-attr,
    .MemberNote_container button.ffe-blog-text-attr {
      float: none;
    }
    
    .MemberNote_container .CommentEditManagement {
      padding: 0;
      margin-top: 10px;
    }
    
    .ffme-no-management .Management.right {
      display: none !important;
    }
    
    #exampleModal.ffme {
      width: 500px;
      margin: 0 auto;
    }
    #exampleModal.ffme-ii {
      width: 600px;
    }
    #exampleModal.ffme-editor {
      width: 100%;
    }
    
    .checkbox {
      height: 14px;
      line-height: 14px;
      padding-left: 20px;
      cursor: pointer;
      position: relative;
    }
    .checkbox:before {
      position: absolute;
      left: 0;
      top: 0;
      width: 14px;
      height: 14px;
      display: block;
      content: "";
      background: url(images/check.gif) 0 0;
      background-repeat: no-repeat;
    }
    .checkbox:hover:before {
      background-position: 0 -28px;
    }
    .checkbox.checked:before {
      background-position: 0 -14px;
    }
    .checkbox.checked:hover:before {
      background-position: 0 -42px;
    }
    .checkbox.disabled {
      color: #aaa;
    }      
    .checkbox.disabled:before {
      background-position: 0 -28px;
    }      
    .checkbox.checked.disabled:before {
      background-position: 0 -42px;
    }      
    
    .ffme_settings_menu li {
      margin: 10px 5px 10px 0;
    }
    .ffme_settings_blocks {
      height: 200px;
    }
    .ffme_settings_block {
      padding: 16px 16px 0;
      text-align: left;
      display: none;
    }
    .ffme_settings_block.active {
      display: block;
    }
    .ffme_settings_block .sub {
      padding: 0 20px;
      margin: 4px 0;
    }
    .ffme_settings_block .checkbox.sub {
      margin-left: 20px;
    }
    .ffme_settings_block .checkbox.sub2 {
      margin-left: 40px;
    }
    .ffme_settings_block > .checkbox + .sub {
      margin-top: -4px;
    }
    .ffme_settings_block .sub span {
      display: inline-block;
      width: 140px;
    }
    .ffme_settings_block .sub b {
      display: inline-block;
      width: 30px;
      text-align: right;
    }
    .ffme_settings_block .checkbox {
      margin: 7px 0 9px;
    }
    .ffme_settings_block .sub.hint {
      font-size: 70%;
      color: #9e9ea6;
    }
    .ffme_settings_block > div:first-child {
      margin-top: 0;
    }
    .ffme_settings_block input[type="range"] {
      margin: 0 4px;
      vertical-align: middle;
      padding: 0;
    }
    .modal-footer.ffme-modal-footer {
      padding: 12px 16px 16px;
    }
    .ffme_settings_block .ffme-search-user {
      min-height: 260px;
    }
    .ffme_settings_block .ffme-insert-image {
      min-height: 220px;
      text-align: center;
    }
    .ffme-insert-image > img {
      position: relative;
      top: 80px;
    }
    .ffme-insert-image > iframe, .ffme-insert-image > .phe-error {
      display: none;
    }
    .ffme-insert-image.phe-err > .phe-error {
      display: block;
    }
    .ffme-insert-image.phe-err > .phe-error > h1 {
      font-size: 72px; color: #333;
    }
    .ffme-insert-image.phe-ok > iframe {
      display: block;
    }
    .ffme-insert-image.phe-ok > img, .ffme-insert-image.phe-err > img {
      display: none;
    }
    .ffme-ii .ffme_settings_block {
      padding: 8px 8px 0;
    }
    
    ul.main_menu a.ffme_support_btn, .HeaderSlideMenu li a.ffme_support_btn {
      float: right;
      margin: 4px 5px 0 0;
      padding: 1px 6px 3px;
      color: #555459;
      background: #efefdb;
      display: inline-block;
      font-size: 11px;
      border-radius: 2px;
      cursor: pointer;
      border: 1px solid #92927e;
    }
    ul.main_menu a.ffme_support_btn:hover, .HeaderSlideMenu li a.ffme_support_btn:hover {
      background: #d8d8c4;
    }
    input[type=button].ffme_settings_btn {
      float: right;
      margin: 4px 5px 0 0;
      padding: 2px 6px;
      color: #555459;    
    }
    .HeaderSlideMenu li a.ffme_support_btn, .HeaderSlideMenu input[type=button].ffme_settings_btn {
      margin-top: 8px;
    } 
    #PrivateMenu input[type=button].ffme_settings_btn {
      margin-top: 5px;
    }
    #PrivateMenu a.ffme_support_btn {
      margin-top: 5px;
      line-height: 14px;
    }
    
    img.ffme-modal-close {
      cursor: zoom-out;
    }
    
    .select2-results__option.select2-results__message, .select2-results__option.loading-results {
      font-size: 12px;
      text-align: center;
    }
    .select2-results__options {
      text-align: left;
      font-size: 12px;
    }
    .select2-container--default .select2-results__option--highlighted[aria-selected] {
      color: initial;
      background: #f3f3e3;
    }
    .ffme-results-user img {
      width: 36px;
      float: left;
      margin-right: 8px;
    }
    .ffme-results-user strong, .ffme-results-user span {
      display: block;
      line-height: 18px;
    }
    .ffme-results-story > strong, .ffme-results-story > span {
      display: block;
      line-height: 18px;
    }
    a i.fa-window-restore, a i.fa-picture-o, a i.fa-video-camera, a i.fa-external-link {
      font-size: 10px;
      margin-right: 2px;
    }
    .ffme--image, .ffme--video {
      margin: 2px 0;
      display: block;
    }
    .ffme-info {
      margin: 8px 2px;
    }
    .ffme-fw-reset-cb {
      margin: -10px 20px 10px;
    }
    .ffme-map-button {
      position: fixed;
      z-index: 7;
      cursor: pointer;
      right: 0; top: 90px;
      padding: 12px 12px 14px;
      color: #9e9ea6;
    }
    .ffme-chapters-map {
      position: fixed;
      right: 0; top: 132px;
      padding: 4px 16px 4px 0;
      border: 1px solid #d8d8c4;
      background-color: #f3f3e3;
      z-index: 7;
      transform: translateX(110%);
      transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
      box-shadow: 0 .5rem 1rem rgba(0,0,0,.25);
      max-height: calc(100% - 264px);
      overflow-y: auto;
    }
    .ffme-chapters-map.active {
      transform: none;
    }
    .ffme-chapters-map ul {
      text-align: left;
    }
    .ffme-chapters-map ul li {
      padding: 2px; 
    }
    .ffme-chapters-map ul li i {
      visibility: hidden;
    }
    .ffme-chapters-map ul li.active i {
      visibility: visible;
    }
    .ffme-chapters-map.ffme-sections li a {
      font-weight:bold;
    }
    .ffme-chapters-map.ffme-sections li.ffme-chapter a {
      font-weight: normal;
    }
    #iknow_2 select { width: 100%; margin: 2px; box-sizing: border-box; }
    #iknow_2 .select2-container { margin: 4px 0; width: 100% !important; }
    #iknow_2 .select2-selection__rendered { line-height: 26px; }
    
    .ffme-editor-container { height: 600px; }
    .ffme-editor-container p { text-align: left !important; }
    .ffme-editor-container div.spoiler { color: #000 !important; cursor: default !important; height: auto !important; }
    .ffme-editor-container div.spoiler * { visibility: visible !important; }
    :root { --ck-spacing-large: 6px !important; }
    .ffme-editor-container .mention[data-mode=tag] {
      display: inline-block;
      padding: 0 2px;
      margin: 0;
      border-radius: 2px;
      background: rgba(0,0,0,0.05);
      color: #069 !important;
      position: relative;
    }
    .ffme-editor-container .mention[data-mode=user] {
      display: inline-block;
      padding: 0;
      margin-top: 0;
      margin-bottom: 0;
      border-radius: 2px;
      color: #339 !important;
      position: relative;
      font-weight: bold;
      background: rgba(0,0,0,0.05);
    }
    .ffme-editor-container .mention[data-mode=tag] + .ffme-editor-container .mention[data-mode=tag] {
      margin-left: 1px;
    }
    .ck-user-mention { display: flex; align-items: center; }
    .ck-user-mention > img { width: 24px !important; height: 24px !important; margin-right: 8px !important; }
    .ck-user-mention > span { max-width: 300px !important; text-overflow: ellipsis !important; overflow-x: hidden; color: #333 !important; }
    .ck-user-mention.ck-on > span { color: #fff !important; }
    .DialogMsg { margin: 0; padding: 4px 0; box-shadow: 0 -1px 0 #D8D8C4; }
    .DialogMsg:last-child { box-shadow: 0 -1px 0 #D8D8C4, 0 1px 0 #D8D8C4; }
    .DialogMsgImp { left: -20px; }
    .DialogMsgImp:before {
      content: "\\f005"; display: inline-block;
      font: normal normal normal 14px/1 FontAwesome;
      font-size: inherit; text-rendering: auto; -webkit-font-smoothing: antialiased;
      color: #ff8f00; margin-left: 2px;
    }
    .DialogMsgImp > img { display: none !important; }
    .DialogMsgTd1, .DialogNewMsgFormTd1 { width: auto; margin: 8px 8px 8px 24px; }
    .DialogMsgTd2 { margin: 4px 0; }
    .DialogMsgTd2 > div:not(.MessageManagementContainer) { position: relative; }
    .DialogMsgTd2 > div:not(.MessageManagementContainer) span.light { color: rgba(183, 28, 28, 0.75); font-size: 70%; display: block; margin: 1px 0 0; line-height: 1.2; }
    .DialogMsgTd2 .CommentEditManagement { padding: 10px 0; }
    .DialogNewMsgFormTd2 { margin: 8px 0 6px; }
    .DialogMsgTd2, .DialogNewMsgFormTd2 { width: auto; flex-grow: 1; -webkit-flex-grow: 1; }
    .DialogMsgTd3, .DialogNewMsgFormTd3 { width: 24px; }
    .DialogNewMsgAtchMenu { padding-top: 2px; }
  `;

  let $btn, $archive;

  if (!isFicWrite) {
    $btn = $('<button class="ffe-blog-text-attr" />');
    $archive = $(`<span class="Management"><a class="small_link light" href="https://archive.today?run=1&url=URL" target="_blank">В архив</a>`);
  }

  const _attachTextButtons = _debounce(attachTextButtons, 100);
  const _parseCommentLinks = _debounce(parseCommentLinks, 150);
  const _updateFicChaptersMap = _debounce(updateFicChaptersMap, 100);
  const _adjustFicChaptersMap = _debounce(adjustFicChaptersMap, 200);
  const _attachAuthorGuesser = _debounce(attachAuthorGuesser, 100);
  const _attachCiteButtons = _debounce(attachCiteButtons, 100);

  const chaptersMap = [];
  let hasSections = false, chapterTitleYDiff = null, hideChaptersMap = true;

  const request = (function () {
    // polyfill xmlhttpRequest
    const xmlHttpRequest = 'GM' in win && 'xmlHttpRequest' in GM
      ? GM.xmlHttpRequest
      : GM_xmlhttpRequest;

    return function (url, method, json) {
      return new Promise((resolve, reject) => {
        const params = {
          url,
          method,
          onerror(response) {
            console.error(response.responseText);
            reject(response.responseText);
          },
          onload(xhr) {
            resolve(json ? JSON.parse(xhr.responseText) : xhr.responseText);
          }
        };
        if (json === true) {
          params.headers = { 'Accept': 'application/json' };
        }
        xmlHttpRequest(params);
      })
    }
  })();

  try {
    addCSS();
    getUser();
    loadSettings();
    addEditorScript();
    moveBannerButton();
    loadCache();
    scrollNext();
    addWindowMessageHandler();
    updateSettingsCss();
    attachSettingsHandlers();
    attachTextButtons();
    reparseMessages();
    attachCiteButtons();
    attachSettingsButton();
    prepareFicWork();
    attachFicWriteButtons();
    attachFicChaptersMap();
    startObserver();
    startFicWorkObserver();
    attachAuthorGuesser();
  } catch (err) {
    win.console.error('Fanfics Extender:', err);
  }

  function addCSS() {
    const head = document.head || document.getElementsByTagName('head')[0];

    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.type = 'text/css';
    link.href = 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.4/css/select2.min.css';
    head.appendChild(link);

    const flink = document.createElement('link');
    flink.rel = 'stylesheet';
    flink.setAttribute('crossorigin', 'anonymous');
    flink.integrity = 'sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN';
    flink.href = 'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css';
    head.appendChild(flink);

    const style = document.createElement('style');
    style.type = 'text/css';
    if (style.styleSheet) {
      style.styleSheet.cssText = CSS;
    } else {
      style.appendChild(document.createTextNode(CSS));
    }
    head.appendChild(style);
  }

  function addEditorScript() {
    if (UID == null || !settingsGet(CONF_RICH_EDITOR) || (!isBlogs && !isAloneOwn && !isOwnProfile)) {
      return;
    }

    const head = document.head || document.getElementsByTagName('head')[0];
    const editor = document.createElement('script');
    editor.src = `${EDITOR_ENDPOINT}ckeditor.ffme.v${EDITOR_VERSION}.js`;
    head.appendChild(editor);
    win.console.info(`Fanfics Extender Editor [${EDITOR_VERSION}]`);
  }

  function updateSettingsCss() {
    if (UID == null) {
      return;
    }
    let css = ``;

    if (settingsGet(CONF_IMAGE_TETHER)) {
      const spoilerOnly = settingsGet(CONF_IMAGE_TETHER_SPOILER);
      let maxWidth = settingsGet(CONF_IMAGE_TETHER_WIDTH),
          maxHeight = settingsGet(CONF_IMAGE_TETHER_HEIGHT);

      let widthStop = 680;
      if (/^\/message\d+/.test(window.location.pathname)) {
        widthStop = 750;
      }
      if (/^\/user\d+/.test(window.location.pathname)) {
        widthStop = 470;
      }
      const widthStopC = widthStop - 57;
      maxWidth = Math.min(maxWidth, widthStop);
      const maxWidthInBlock = Math.min(maxWidth, widthStop - 42);
      const maxWidthIn2Blocks = Math.min(maxWidth, widthStop - 42 * 2);

      const maxWidthC = Math.min(maxWidth, widthStopC);
      const maxWidthSpoilerC = Math.min(maxWidthC, widthStopC - 42);
      const maxWidthSpoilerC2 = Math.min(maxWidthC, widthStopC - 42 * 2);

      if (!spoilerOnly) {
        css += `
         .MessageText img:not(.expanded), .MyCommentsItemText img:not(.expanded) {
            max-width: ${maxWidth}px;
            max-height: ${maxHeight}px;
          }
         [id^="MessageComments_"] .MessageText img:not(.expanded), [id^="MessageComments_"] .MyCommentsItemText img:not(.expanded) {
            max-width: ${maxWidthC}px;
          }
        `;
      }
      css += `
        .MessageText .spoiler img:not(.expanded), .MyCommentsItemText .spoiler img:not(.expanded),
        .MessageText .blockquote img:not(.expanded), .MyCommentsItemText .blockquote img:not(.expanded) {
          max-width: ${maxWidthInBlock}px;
          max-height: ${maxHeight}px;
        }
        .MessageText .spoiler .blockquote img:not(.expanded), .MyCommentsItemText .spoiler .blockquote img:not(.expanded),
        .MessageText .blockquote .spoiler img:not(.expanded), .MyCommentsItemText .blockquote .spoiler img:not(.expanded) {
          max-width: ${maxWidthIn2Blocks}px;
          max-height: ${maxHeight}px;
        }
        [id^="MessageComments_"] .MessageText .spoiler img:not(.expanded),
        [id^="MessageComments_"] .MyCommentsItemText .spoiler img:not(.expanded),
        [id^="MessageComments_"] .MessageText .blockquote img:not(.expanded),
        [id^="MessageComments_"] .MyCommentsItemText .blockquote img:not(.expanded) {
          max-width: ${maxWidthSpoilerC}px;
          max-height: ${maxHeight}px;
        }
        [id^="MessageComments_"] .MessageText .spoiler .blockquote img:not(.expanded),
        [id^="MessageComments_"] .MyCommentsItemText .spoiler .blockquote img:not(.expanded),
        [id^="MessageComments_"] .MessageText .blockquote .spoiler img:not(.expanded),
        [id^="MessageComments_"] .MyCommentsItemText .blockquote .spoiler img:not(.expanded) {
          max-width: ${maxWidthSpoilerC2}px;
          max-height: ${maxHeight}px;
        }
      `;
    }

    if (settingsGet(CONF_IMAGE_ZOOM_BY_CLICK)) {
      css += `
        .MessageText img, .MyCommentsItemText img {
          cursor: zoom-in;
        }
        .MessageText img:hover, .MyCommentsItemText img:hover {
          box-shadow: 0 0 1px 1px #9e9ea6;
        }
      `;
    }

    if (settingsGet(CONF_SPOILER_COLLAPSE_HIDDEN)) {
      css += `
        .spoiler:not(.selected) {
          height: 16px;
          overflow: hidden;
        }
        .CommentItemRight .spoiler:not(.selected) {
          height: 14px;
          overflow: hidden;
        }
        .MessageText[style^="height:176px"] {
          height: initial !important;
          max-height: 176px;
        }
      `;
    }

    if (settingsGet(CONF_FULL_MENU)) {
      css += `
        .main_menu2 { display: block !important; }
      `;
    }

    const makeSelectors = function (selectors, pseudo, prepend) {
      return selectors.map(function(s) { return `${prepend?`${prepend} `:''}${s}${pseudo?`:${pseudo}`:''}`; }).join(', ');
    };

    if (settingsGet(CONF_LINKS_HIGHLIGHT)) {
      const list = [
        `.MessageText a:not(.small_link):not(.user):not(.light)`,
        `.MyCommentsItemText a:not(.small_link):not(.user):not(.light)`,
      ];
      css += `
        ${makeSelectors(list)}, ${makeSelectors(list, 'visited')} {
          color: #069;
          text-decoration: underline;
        }
        ${makeSelectors(list,'hover')}, ${makeSelectors(list,'active')} {
          color: #f03;
          text-decoration: underline;
        }
      `;
    }

    if (settingsGet(CONF_TAGS_HIGHLIGHT)) {
      const sel = `#site-content-center a[href*="?search=%23"]:not(.small_link):not(.light):not([href*="go.php"])`;
      css += `
        ${sel}, ${sel}:visited {
          display: inline-block;
          padding: 0 2px;
          margin: 0 0 1px;
          border-radius: 2px;
          background: rgba(0,0,0,0.05);
          color: #069 !important;
          position: relative;
          text-decoration: none;
        }
        ${sel}:hover, ${sel}:active {
          color: #069 !important;
          background: rgba(0,0,0,0.1);
        }
        ${sel} + ${sel} {
          margin-left: 1px;
        }
      `;
    }

    if (settingsGet(CONF_LINKS_FF_HIGHLIGHT)) {
      const list = [
        'a[href^="/go.php?url=https://fanfics.me/ftf"]',
        'a[href^="/go.php?url=https://fanfics.me/fic"]',
        'a[href^="https://fanfics.me/ftf"]',
        'a[href^="https://fanfics.me/fic"]',
        'a[href^="/ftf"]',
        'a[href^="/fic"]',
        'a[href^="/go.php?url=http://samlib.ru/"]',
        'a[href^="/go.php?url=https://ficbook.net/"]',
        'a[href^="/go.php?url=http://snapetales.com/"]',
        'a[href^="/go.php?url=http://www.snapetales.com/"]',
        'a[href^="/go.php?url=http://hogwartsnet.ru/"]',
        'a[href^="/go.php?url=https://hogwartsnet.ru/"]',
        'a[href^="/go.php?url=https://www.fanfiction.net/"]',
        'a[href^="/go.php?url=http://archiveofourown.org/"]',
        'a[href^="/go.php?url=https://archiveofourown.org/"]',
        'a[href^="/go.php?url=https://lit-era.com/"]',
      ];
      css += `
        ${makeSelectors(list, false, '.MyCommentsItemText')},
        ${makeSelectors(list, false, '.MessageText')},
        ${makeSelectors(list,'visited', '.MyCommentsItemText')},
        ${makeSelectors(list,'visited', '.MessageText')} {
          display: inline-block;
          padding: 0 2px;
          margin: 0 0 1px;
          border-radius: 2px;
          background: rgba(0,0,0,0.05);
          color: #093 !important;
          position: relative;
          text-decoration: none !important;
        }
        ${makeSelectors(list,'hover','.MyCommentsItemText')},
        ${makeSelectors(list,'hover', '.MessageText')},
        ${makeSelectors(list,'active', '.MyCommentsItemText')},
        ${makeSelectors(list,'active', '.MessageText')} {
          background: rgba(0,0,0,0.1);
          color: #093 !important;
        }
      `;
    }

    if (settingsGet(CONF_LINKS_R_HIGHLIGHT)) {
      const list = [
        'a[href^="/go.php?url=https://fanfics.me/request"]',
        'a[href^="https://fanfics.me/request"]',
      ];
      css += `
        ${makeSelectors(list, false, '.MyCommentsItemText')},
        ${makeSelectors(list, false, '.MessageText')},
        ${makeSelectors(list,'visited', '.MyCommentsItemText')},
        ${makeSelectors(list,'visited', '.MessageText')} {
          display: inline-block;
          padding: 0 2px;
          margin: 0 0 1px;
          border-radius: 2px;
          background: rgba(0,0,0,0.05);
          color: #ef6c00 !important;
          position: relative;
          text-decoration: none !important;
        }
        ${makeSelectors(list,'hover','.MyCommentsItemText')},
        ${makeSelectors(list,'hover', '.MessageText')},
        ${makeSelectors(list,'active', '.MyCommentsItemText')},
        ${makeSelectors(list,'active', '.MessageText')} {
          background: rgba(0,0,0,0.1);
          color: #ef6c00 !important;
        }
      `;
    }

    if (settingsGet(CONF_LINKS_U_HIGHLIGHT)) {
      // .MyCommentsItemText
      // .MessageText
      const list = [
        'a.user:not(.small)'
      ];
      css += `
        ${makeSelectors(list, false, '.MyCommentsItemText')},
        ${makeSelectors(list, false, '.MessageText')},
        ${makeSelectors(list,'visited', '.MyCommentsItemText')},
        ${makeSelectors(list,'visited', '.MessageText')} {
          display: inline-block;
          padding: 0;
          margin-top: 0;
          margin-bottom: 0;
          border-radius: 2px;
          color: #339 !important;
          position: relative;
          text-decoration: none !important;
          font-weight: bold;
        }
        ${makeSelectors(list,'hover','.MyCommentsItemText')},
        ${makeSelectors(list,'hover', '.MessageText')},
        ${makeSelectors(list,'active', '.MyCommentsItemText')},
        ${makeSelectors(list,'active', '.MessageText')} {
          color: #339 !important;
          background: rgba(0,0,0,0.1);
          box-shadow: -2px 0 0 0 rgba(0,0,0,0.1), 2px 0 0 0 rgba(0,0,0,0.1);
        }
      `;
    }

    if (settingsGet(CONF_FIX_LIKES_MODAL)) {
      css += `
        #likesModal { width: 780px; margin: 0 auto; }
        #likesModal .LikesList { min-height: initial; }
        #likesModal .LikesList:after { content: ""; display: table; clear: both; }
        #likesModal .LikesList > div:not(.clear) { width: 260px !important; height: 34px !important; float: left; text-align: left; margin: 0; padding: 5px; box-sizing: border-box; text-overflow: ellipsis; white-space: nowrap; }
        #likesModal .LikesList > div.clear { display: none; }
        #likesModal .LikesList > div > a:first-child { float: left; display: block; }
        #likesModal .LikesList > div img { width: 24px !important; height: 24px !important; margin: 0; vertical-align: bottom; background-size: contain; }
        #likesModal .LikesList > div > br { display: none; }
        #likesModal .LikesList > div > a:last-child { line-height: 24px; padding-left: 4px; }
      `;
    }

    const head = document.head || document.getElementsByTagName('head')[0];
    let style = document.getElementById('ffme-settings-styles');
    if (style == null) {
      style = document.createElement('style');
      style.type = 'text/css';
      style.id = "ffme-settings-styles";
      if (style.styleSheet) {
        style.styleSheet.cssText = css;
      } else {
        style.appendChild(document.createTextNode(css));
      }
      head.appendChild(style);
    } else {
      style.textContent = css;
    }
  }
  let inEditing = false;
  const preventUnloadText = 'Набранный текст может быть утерян!';
  // let pheMessageHandler = null;
  function addWindowMessageHandler() {
    window.addEventListener('beforeunload', function (event) {
      if (inEditing) {
        event.preventDefault();
        event.returnValue = preventUnloadText;
        return preventUnloadText;
      }
    });
    const testRE = /^https:\/\/fanfics\.me$/;
    win.addEventListener('message', function (event) {
      if (testRE.test(event.origin)) {
        if (event.data.substr(0, 1) !== '{') {
          switch (event.data) {
            case '_attachTextButtons':
              _attachTextButtons();
              break;
            case 'reparseMessages':
              _defer(reparseMessages);
              break;
            default:
              break;
          }
        } else {
          /*
          const msg = JSON.parse(event.data);
          switch (msg.t) {
            default:
              break;
          }
          */
        }
      }
      /*
      if (/^https?:\/\/www\.pichome\.ru$/.test(event.origin)) {
        if (pheMessageHandler == null) {
          return;
        }
        try {
          const phemsg = JSON.parse(event.data);
          pheMessageHandler(phemsg);
        } catch (err) {

        }
      }
      */
    }, false);
  }

  function moveBannerButton() {
    if (UID == null || !settingsGet(CONF_MOVE_BANNER)) {
      return null;
    }
    const b = document.querySelector(`a[href="/user${UID}?action=personal_card"]`);
    if (!b) {
      return;
    }
    const p = b.parentNode;
    p.removeChild(b);
    p.appendChild(b);
  }

  function scrollNext() {
    if (settingsGet(CONF_SCROLL_NEXT)) {
      settingsSet(CONF_SCROLL_NEXT, false, true);
      const p = window.location.pathname;
      if (p !== '/read.php' && p !== '/read2.php') {
        return;
      }
      const params = {};
      window.location.search.substr(1).split('&').forEach(function (pair) {
        const spl = pair.split('=');
        params[spl[0]] = spl[1].replace(/\+/g, ' ');
      });
      let c = null;
      if (params.chapter != null) {
        c = params.chapter;
      } else if (params.id != null) {
        c = 0;
      }
      if (c != null) {
        const $c = $(`#c${c}`).prevAll('h2');
        $('html, body').animate({
          scrollTop: $c.offset().top - 50
        }, 500);
      }
    }
  }

  function attachSettingsHandlers() {
    if (UID == null) {
      return null;
    }
    $('.MessageText img, .MyCommentsItemText img').live('click', function(e){
      if (settingsGet(CONF_IMAGE_ZOOM_BY_CLICK)) {
        e.preventDefault();
        e.stopPropagation();
        const html = `<img src="${$(this).attr('src')}" class="ffme-modal-close ffme-modal-global" />`;
        $.arcticmodal({
          openEffect: { type: 'none' }, closeEffect: { type: 'none' },
          content: html,
        });
      }
    });

    $(_youtubeSelectors()).live('click', function (e){
      if (settingsGet(CONF_LINKS_YOUTUBE) && settingsGet(CONF_LINKS_YOUTUBE_POPUP) && !(isAlone && settingsGet(CONF_LINKS_YOUTUBE_ALONE))) {
        e.preventDefault();
        e.stopPropagation();
        const link = this.getAttribute('href');
        _createYoutubeFrame(link).then(function ($html) {
          $.arcticmodal({
            openEffect: { type: 'none' }, closeEffect: { type: 'none' },
            content: $html,
          });
        }).catch(console.log.bind(console));
      }
    });
    $(_coubSelectors()).live('click', function (e){
      if (settingsGet(CONF_LINKS_COUB) && settingsGet(CONF_LINKS_COUB_POPUP) && !(isAlone && settingsGet(CONF_LINKS_COUB_ALONE))) {
        e.preventDefault();
        e.stopPropagation();
        const link = this.getAttribute('href');
        _createCoubFrame(link).then(function ($html) {
          $.arcticmodal({
            openEffect: { type: 'none' }, closeEffect: { type: 'none' },
            content: $html,
          });
        }).catch(console.log.bind(console));
      }
    });
    $('.spoiler a').live('click', function (e) {
      e.stopPropagation();
    });
    $('.spoiler').die('click').live('click', function (e) {
      const sel = win.getSelection();
      if (!sel.isCollapsed) {
        const $spoiler = $(sel.anchorNode).closest('.spoiler');
        if ($spoiler.length && $spoiler.get(0) === e.target) {
          return;
        }
      }
      $(e.target).toggleClass('selected');
    });

    $('#FicReadLink a, .FicContents a, #next_page, #previous_page').live('click', function () {
      if (settingsGet(CONF_NAV_SCROLL)) {
        settingsSet(CONF_SCROLL_NEXT, true, true);
      }
    });

    if (isAlone && document.querySelector('.MessageCommentNewInput') != null) {
      const ownerName = $('.MessageAloneHeadRight > [data-show-member] > a').text().replace(/'/g, "\\'");
      if (ownerName !== userName) {
        const $aloneButtons = $('.MessageAlone > table .MessageButtons');
        const mid = $aloneButtons.attr('id').split('_').pop();
        const $btnReply = $(`<span class="Management" style="padding-right: 10px;"><a class="small_link light" onclick="insnameblog(${mid}, '${ownerName}')">Ответить</a></span>`);
        $aloneButtons.append($btnReply);
      }
    }

    updateCustomSettings();

    window.addEventListener('keydown', function (e) {
      if (e.defaultPrevented || !settingsGet(CONF_KBD_NAV)) {
        return;
      }
      if (e.target != null) {
        const tag = e.target.tagName.toLowerCase();
        if (tag === 'textarea' || tag === 'input') {
          return;
        }
      }
      if (isMac ? !e.metaKey : !e.ctrlKey) {
        return;
      }
      let prevnext = null;
      if (e.key != null) {
        if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
          prevnext = e.key === 'ArrowRight';
        }
      } else if (e.keyCode != null) {
        if (e.keyCode === 37 || e.keyCode === 39) {
          prevnext = e.keyCode === 39;
        }
      } else if (e.code != null) {
        if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
          prevnext = e.code === 'ArrowRight';
        }
      }
      if (prevnext != null) {
        const $a = $(`#${prevnext?'next':'previous'}_page`);
        if ($a.length) {
          if (settingsGet(CONF_NAV_SCROLL)) {
            settingsSet(CONF_SCROLL_NEXT, true, true);
          }
          window.location = $a.attr('href');
        }
        e.preventDefault();
      }
    }, true);

    $('.ffme-modal-close').live('click', function (e) {
      e.preventDefault();
      if (e.target.classList.contains('ffme-modal-global')) {
        $.arcticmodal('close');
      } else {
        $('#exampleModal').arcticmodal('close');
      }
    });

    // execute in page context
    // overwriting page functions

    win.eval('window._insertAtCaret = $.fn.insertAtCaret');
    win.eval(`$.fn.insertAtCaret = function (a, b) {
      return _insertAtCaret.apply(this, [window.overwriteA !== false ? window.overwriteA : a, b]);
    }`);

    win.eval('window._insname = window.insname');
    win.eval('window._insnameblog = window.insnameblog');
    win.eval(`window.insname = function (name) {
      if (ffme_settings.${CONF_ONELINE_ADDRESS}) {
        window.overwriteA = \`\${name}, \`;
      }
      const ret = _insname.apply(this, [name]);
      window.overwriteA = false;
      return ret;
    }`);
    win.eval(`window.insnameblog = function (message_id, name) {
      if (ffme_settings.${CONF_ONELINE_ADDRESS}) {
        window.overwriteA = \`{\${name}}, \`;
      }
      const ret = _insnameblog.apply(this, [message_id, name]);
      window.overwriteA = false;
      return ret;
    }`);

    win.eval('window._editcomment = window.editcomment');
    win.eval('window._editcomment_cancel = window.editcomment_cancel');
    win.eval(`window.editcomment = function (id, subject, section) {
      pauseObserve();
      const mg = document.querySelector(\`#MessageTable_\${id}.Message.alone\`);
      if (mg != null) {
        mg.classList.add('ffme-no-management');
      }
      const hooks = beforeEdit[\`\${subject}_\${id}\`] || [];
      hooks.forEach(function (h) {
        const p = h.e.parentNode;
        if (h.a != null) {
          h.e.setAttribute(h.a, h.v);
        } else if (h.r != null) {
          if (p != null) {
            p.replaceChild(h.r, h.e);
          }
        } else if (h.d === true) {
          if (p != null) {
            p.removeChild(h.e);
          }
        } else if (h.rc != null) {
          h.e.classList.remove(h.rc);
          if (!h.e.classList.length) {
            h.e.removeAttribute('class');
          }
        } else if (h.t != null) {
          h.e.textContent = h.t;
        }
      });
      delete beforeEdit[\`\${subject}_\${id}\`];
      const ret = _editcomment.apply(this, [id, subject, section]);
      _defer(function(){
        ffmeCall('_attachTextButtons');
        resumeObserve(true);
        if (mg != null) {
          const form = document.querySelector(\`#messageEditForm_\${id}\`);
          form.addEventListener('submit', function () {
            setTimeout(function () {
              if (!form.parentNode) {
                mg.classList.remove('ffme-no-management');
              }
            }, 1000);
          });
        }
      });
      return ret;
    }`);
    win.eval(`window.editcomment_cancel = function (id, subject) {
      const mg = document.querySelector(\`#MessageTable_\${id}.Message.alone\`);
      if (mg != null) {
        mg.classList.remove('ffme-no-management');
      }
      const ret = _editcomment_cancel.apply(this, [id, subject]);
      ffmeCall('reparseMessages');
      return ret;
    }`);
    win.eval('window._php_success = window.php.success');
    win.eval(`window.php.success = function (_a, _b) {
      if (_a == null) return;
      const json = JSON.stringify(_a).replace(/>Мои хэштеги/, " title='Мои хэштеги'>#");
      _php_success(JSON.parse(json), _b);
    }`);
  }

  function updateCustomSettings() {
    if (settingsGet(CONF_BLOG_AUTO_DST)) {
      const loc = window.location;
      if (loc.pathname === `/user${UID}`) {
        win.eval('messageNewManagementPrivate(0)');
      } else if (loc.pathname === '/blogs' && (loc.search == null || loc.search === '')) {
        win.eval('messageNewManagementPrivate(1)');
      }
    }
  }

  function _checkButtonsContainer($container) {
    return !($container.find('.ffe-blog-text-attr').length);
  }

  function attachTextButtons() {
    if (isFicWrite) return;
    pauseObserve();
    const $msgButtonsContainer = $('.MessageNewInfoHead');
    if ($msgButtonsContainer.length && _checkButtonsContainer($msgButtonsContainer)) {
      const helpText = 'Справка';
      $msgButtonsContainer.children(`a.small_link:contains(${helpText})`).remove();//.html('?').prop('title', helpText);
      const nbsp = $msgButtonsContainer.get(0).firstChild;
      if (nbsp.nodeType === 3) {
        nbsp.parentNode.removeChild(nbsp);
      }
      const $before = $msgButtonsContainer.children('a.small_link:contains(Мои хэштеги)').html('#').prop('title', 'Мои хэштеги');
      $msgButtonsContainer.children(`a.small_link:contains(Редактор)`).remove();
      const oldSubmit = $msgButtonsContainer.prev('input[type=submit]');
      $btn.clone().attr('type', 'submit').attr('title', 'Отправить').html('<i class="fa fa-send"></i>').insertBefore(oldSubmit);
      oldSubmit.remove();
      _createButtons('#MessageNew', $before, '!');
    }

    const $commentButtonsContainer = $('#new_comment');
    if ($commentButtonsContainer.length && _checkButtonsContainer($commentButtonsContainer)) {
      const $before = $('<span class="clear">');
      $commentButtonsContainer.find('#comment_button_span').append($before);
      _createButtons('#text_comment', $before, 'q,sp,c');
    }

    const $dialogButtonsContainer = $('#DialogNewMsgButton');
    if ($dialogButtonsContainer.length && _checkButtonsContainer($dialogButtonsContainer)) {
      const $before = $('.DialogNewMsgAtchMenu');
      _createButtons('#DialogNewMsgTextarea', $before);
    }

    const $msgNewCommentContainer = $('.MessageCommentNewInstr');
    $msgNewCommentContainer.each(function (idx, elem) {
      const $e = $(elem);
      if (!_checkButtonsContainer($e)) {
        return;
      }
      const $clear = $e.children('.clear');
      const id = $e.attr('id').split('_').pop();
      const selector = `#MessageCommentNewTextarea_${id}`;
      _createButtons(selector, $clear, '+');
    });
    const $msgEditContainer = $('.CommentEditManagement');
    $msgEditContainer.each(function (idx, elem) {
      const $e = $(elem);
      if (!_checkButtonsContainer($e) || (elem.id != null && /^membernoteEditManagement_\d+$/.test(elem.id))) {
        return;
      }
      const allButtons = /^message_?/.test($e.attr('id'));
      const $cancel = $e.children('.modern_button_cancel');
      const selectorId = $e.prevAll('textarea[id]').attr('id');
      const selector = `#${selectorId}`;
      _createButtons(selector, $cancel, allButtons ? (isAloneOwn ? '!' : false) : 'q,sp,c');
    });

    if (/^\/user\d+/.test(window.location.pathname)) {
      const $noteContainer = $('.MemberNote_container [id^="membernoteEditManagement_"]');
      if ($noteContainer.length && _checkButtonsContainer($noteContainer)) {
        const $before = $('<span class="clear">');
        $before.insertBefore($noteContainer.find('.modern_button_cancel'));
        _createButtons('[name="text_membernote"]', $before, '*b,i,s,c');
      }
    }

    resumeObserve();
  }

  function insciteblogext(event, cid, mid, username) {
    const sel = document.getSelection();
    let selcid = null;
    if (!sel.isCollapsed) {
      const $t = $(sel.anchorNode).closest('.MessageText');
      if ($t.length) {
        selcid = $t.get(0).id.split('_').pop();
      }
    }
    // prepare text for cite
    let cite = '';
    if (String(cid) === String(selcid)) {
      cite = sel.toString();
    } else {
      const clone = document.getElementById(`message_comment_${cid}`).cloneNode(true);
      const tags = ['b','i','s','a','img'];
      Array.from(clone.childNodes).forEach(function (n) {
        if (n.nodeType !== 3) {
          const nn = n.nodeName.toLowerCase();
          let del = false;
          if (!~tags.indexOf(nn)) {
            del = true;
          } else {
            switch (nn) {
              case 'a':
                if (n.classList.contains('user')) {
                  del = true;
                  break;
                }
                let t = n.textContent;
                const h = n.getAttribute('href').replace(/^.*?\/go\.php\?url=/, '');
                if (t !== h) {
                  t = `[${t}|${h}]`;
                }
                n.textContent = t;
                break;
              case 'img':
                n.textContent = n.getAttribute('src');
                break;
              default:
                n.textContent = `[${nn}]${n.textContent}[/${nn}]`;
                break;
            }
          }
          if (del) {
            clone.removeChild(n);
          }
        }
      });
      cite = clone.textContent;
    }
    cite = cite.trim();
    if (cite.substr(0,1) === ',') {
      cite = cite.substr(1).trim();
    }
    if (!cite.length) {
      _defer(function () {
        insnameblog(mid, username.replace(/'/, '\\\''));
      });
      return;
    }
    const $ta = $(`#MessageCommentNewTextarea_${mid}`);
    let prepend = '';
    if (!$ta.is(':visible')) {
      blogcommentopen(mid);
    } else {
      const last = $ta.val().substr(-1);
      if (last && last !== "\r" && last !== "\n") {
        prepend = event.altKey ? "\n" : "\n\n";
      }
    }
    const u = event.altKey ? '' : `{${username}}\n`;

    $ta.insertAtCaret(`${prepend}${u}[q]${cite}[/q]\n`).keydown();
    _defer(function (){ $ta.focus(); });

  }
  win.eval(`window.insciteblogext = ${insciteblogext.toString()};`);

  function attachCiteButtons() {
    if (UID == null) return;

    const links = document.querySelectorAll('.MessageBody .MessageButtons > a.small_link[onmousedown^="insciteblog("]');
    links.forEach(function (a) {
      a.setAttribute('onmousedown', a.getAttribute('onmousedown').replace('insciteblog(', 'insciteblogext(event,'));
    });
  }

  function _resetFwResetCheckbox() {
    const cb = document.querySelector('.ffme-fw-reset-cb');
    const on = [].concat(...document.querySelectorAll('.TextFromWord0')).some(function (el) { return el.style.display === 'block'; });
    cb.classList.remove('checked');
    cb.classList[!on?'add':'remove']('hidden');
  }

  function gword_filter_traverse(el) {
    let tag = '';
    const st = el.style;
    if (st.textAlign === 'right') {
      tag = 'right';
    } else if (st.textAlign === 'center') {
      tag = 'center';
    }

    let line = '';

    const cnt = el.childNodes.length;
    el.childNodes.forEach(function (c, i) {
      let t = c.textContent;
      if (!t) return;
      const sc = c.style;
      let wrap = '';
      if (sc.fontWeight === '700') {
        wrap = 'b';
      } else if (sc.fontStyle === 'italic') {
        wrap = 'i';
      } else if (sc.textDecoration === 'underline' || sc.textDecorationLine === 'underline') {
        wrap = 'u';
      } else if (sc.textDecoration === 'line-through' || sc.textDecorationLine === 'line-through') {
        wrap = 's';
      }
      t = t.replace(/\s+/g, ' ');
      if (!i) {
        t = t.replace(/^\s+/, '');
      }
      if (i === cnt-1) {
        t = t.replace(/\s+$/, '');
      }
      line += wrap ? `<${wrap}>${t}</${wrap}>` : t;
    });

    line = line.trim();

    if (!line) return false;

    return tag ? `<${tag}>${line}</${tag}>` : line;
  }

  function gword_filter() {
    const editor = $('#geditor'), content = editor.html().trim();
    if(content === '' || content === '<br>') return false;
    const ge = editor.get(0);
    // check for Word text
    if (/<w:WordDocument>/.test(content)) {
      $('#editor').html(editor.html());
      editor.html('');
      window.word_filter();
      $('#geditor_container').hide();
      $('#editor_container').hide();
      _resetFwResetCheckbox();
      return;
    }
    if (ge.childNodes.length !== 1 || ge.childNodes[0].id == null || !/^docs-internal-guid/.test(ge.childNodes[0].id)) return false;

    let addnl = true;
    const body = ge.childNodes[0];

    // check & fix incomplete paragraph

    let incomplete = ge.querySelectorAll(`#${body.id} > span`);
    if (incomplete.length > 0) {
      const p = document.createElement('p');
      body.insertBefore(p, incomplete[0]);
      incomplete.forEach(function (el) {
        p.appendChild(body.removeChild(el));
      });
      addnl = false;
    }

    const cn = document.querySelector('#chapterName');
    let chapterName = '';
    const lines = [];
    body.childNodes.forEach(function (el) {
//      if (!el.textContent.trim()) return;

      let line = '';
      let ltag = '';
      switch (el.tagName.toLowerCase()) {
        case 'h1':
          ltag = 'h1';
          line = el.textContent;
          if (!chapterName && cn != null) {
            chapterName = line.trim();
            line = '';
          }
          break;
        case 'hr':
          line = '<hr>';
          break;
        case 'p':
          line = gword_filter_traverse(el);
          break;
        default:
          line = el.textContent;
          break;
      }
      if (line !== false) {
        line = line.trim();
        if (line) {
          lines.push(ltag ? `<${ltag}>${line}</${ltag}>` : line);
        }
      }
    });

    if (!lines.length) return false;

    let val = $('#chapterText').val();

    if (document.querySelector('.ffme-fw-reset-cb').classList.contains('checked')) {
      val = '';
    }

    if (chapterName) {
      if (cn != null) {
        cn.value = chapterName;
      }
    }

    $('#chapterText').val(val + lines.join('\n') + (addnl?'\n':''));
    $('#geditor').html('');
    $('#geditor_container').toggle();
    $('#chapterText').focus();

    _resetFwResetCheckbox();
  }

  function prepareFicWork() {
    if (!isFicWrite) return;

    win.eval(`window._resetFwResetCheckbox = ${_resetFwResetCheckbox.toString()}`);

    win.eval(`window._word_filter = window.word_filter`);
    win.eval(`window.word_filter = function () {
      const editor = $('#editor'), content = editor.html().trim();
      if(content === '' || content === '<br>') return false;
      if (document.querySelector('.ffme-fw-reset-cb').classList.contains('checked')) {
        $('#chapterText').val('');
      }
      _word_filter();
      _resetFwResetCheckbox();
    }`);

    win.eval(`window.gword_filter_traverse = ${gword_filter_traverse.toString()}`);
    win.eval(`window.gword_filter = ${gword_filter.toString()}`);
  }

  function attachFicWriteButtons() {
    if (!isFicWrite) return;
    const cont = document.querySelector('.TextEditorContainer');
    if (cont == null || cont.classList.contains('ffme-parsed')) return;

    win.eval(`$('#editor').die('keyup click')`);
    win.eval(`$('#editor').live('keyup click', word_filter)`);

    win.eval(`$('#geditor').die('keyup click')`);
    win.eval(`$('#geditor').live('keyup click', gword_filter)`);

    const copyFromWord = cont.querySelector('#copy_from_word');
    copyFromWord.textContent = copyFromWord.textContent.replace('текст ', '');
    copyFromWord.setAttribute('onclick', "$('#editor_container').toggle(); $('#geditor_container').hide(); _resetFwResetCheckbox();");
    const $copyFromGDocs = $(copyFromWord).clone();
    const copyFromGDocs = $copyFromGDocs.get(0);
    copyFromGDocs.textContent = copyFromGDocs.textContent.replace('MS Word', 'GDocs');
    copyFromGDocs.setAttribute('onclick', "$('#geditor_container').toggle(); $('#editor_container').hide(); _resetFwResetCheckbox();");
    copyFromWord.parentNode.insertBefore(copyFromGDocs, copyFromWord.nextElementSibling);

    const $editor = $('#editor_container');
    const $geditor = $editor.clone();
    $geditor.attr('id', 'geditor_container');
    $geditor.find('#editor').attr('id', 'geditor');
    const gehtml = $geditor.find('.TextFromWord1').html()
      .replace('программы Microsoft Word', 'Google Docs')
      .replace('добавлен', '<br>добавлен');
    $geditor.find('.TextFromWord1').html(gehtml);

    const ehtml = $editor.find('.TextFromWord1').html()
      .replace('и он', '<br>и он');
    $editor.find('.TextFromWord1').html(ehtml);

    copyFromWord.parentNode.insertBefore($geditor.get(0), document.querySelector('#chapterText'));

    const resetWhat = document.querySelector('#chapterName') != null ? 'главы' : 'всех глав';
    const $cb = $(`<div class="checkbox light hidden ffme-fw-reset-cb">Предварительно очистить текст ${resetWhat}</div>`).on('click', function () { this.classList.toggle('checked'); });
    copyFromWord.parentNode.insertBefore($cb.get(0), document.querySelector('#chapterText'));

    cont.classList.add('ffme-parsed');
  }

  function getTopBarAdjustment() {
    const topBar = document.querySelector('.topbar2');
    let h = 0;
    if (topBar.classList.contains('fixed')) {
      h = topBar.getBoundingClientRect().height;
    }
    return h;
  }

  function updateFicChaptersMap() {
    const pos = Math.ceil(window.scrollY + getTopBarAdjustment());
    const $lis = $('.ffme-chapters-map').find('li');
    $lis.removeClass('active');
    let idx = null;
    for (let i = 0; i < chaptersMap.length; i++) {
      if (chaptersMap[i].h <= pos) {
        idx = i;
      } else {
        break;
      }
    }
    if (idx != null) {
      $lis.eq(idx).addClass('active');
    }
  }

  function adjustFicChaptersMap() {
    chaptersMap.forEach(function (c) {
      c.h = c.el.offsetTop - chapterTitleYDiff;
    });
    _updateFicChaptersMap();
  }

  function attachFicChaptersMap() {
    if (!isFicReadAll) return;

    const headers = document.querySelectorAll('.ReadTextContainer > .ReadContent:not([id]) > h2');

    headers.forEach(function (el) {
      if (chapterTitleYDiff == null) {
        chapterTitleYDiff = parseInt(win.getComputedStyle(el).marginTop, 10);
      }
      const t = el.textContent.trim();
      const h = el.offsetTop - chapterTitleYDiff;
      const next = el.nextElementSibling;
      const chapter = { t, h, el };
      if (next.tagName.toLowerCase() === 'h2' || !next.id) {
        // section
        hasSections = true;
      } else {
        chapter.cid = next.id;
      }
      chaptersMap.push(chapter);
    });
    hideChaptersMap = chaptersMap.filter((c) => c.cid != null).length < 2;
    if (hideChaptersMap) {
      return;
    }

    win.addEventListener('scroll', _updateFicChaptersMap);
    win.addEventListener('resize', _adjustFicChaptersMap);
    //  adjust due ads :(
    window.addEventListener('load', _adjustFicChaptersMap);

    const $div = $('<div/>').addClass('ffme-chapters-map');
    if (hasSections) {
      $div.addClass('ffme-sections');
    }
    const $ul = $('<ul/>').appendTo($div);

    chaptersMap.forEach(function (c) {
      const $li = $(`<li><i class="fa fa-fw fa-caret-right"></i><a href="#"></a></li>`);
      $li.find('a').text(c.t);
      if (c.cid != null) {
        $li.addClass('ffme-chapter');
      }
      $ul.append($li);
    });
    $ul.find('a').on('click', function (e) {
      e.preventDefault();
      const idx = $(e.target).closest('li').index();
      const chapter = chaptersMap[idx];
      const h = chapter.h - getTopBarAdjustment();
      document.querySelector('html').scrollTop = h;
    });
    const $mapBtn = $('<div class="ffme-map-button"><i class="fa fa-fw fa-map-o"></i></div>');
    $mapBtn.on('click', function (e) {
      e.preventDefault();
      $div.get(0).classList.toggle('active');
    });
    $('body').prepend($div, $mapBtn);
    $('.edit-font,.edit-width').on('click', _adjustFicChaptersMap);
  }

  function _addEditHook(id, o) {
    let hooks = beforeEdit[id];
    if (beforeEdit[id] == null) {
      beforeEdit[id] = [o];
    } else {
      hooks.push(o);
    }
  }
  /*
  function _getEditHooks(id, subject) {
    return beforeEdit[`${subject}_${id}`] || [];
  }
  function _clearEditHooks(id, subject) {
    delete beforeEdit[`${subject}_${id}`];
  }
  */
  function _adjustSize(size, maxWidth, minWidth) {
    let w = size[0], h = size[1];
    if (maxWidth != null) {
      const max = Math.floor(maxWidth);
      if (w > max) {
        const d = max/w;
        w *= d;
        h *= d;
      }
    }
    if (minWidth != null && w < minWidth) {
      // prevent hiding play/pause button
      const d = minWidth/w;
      w *= d;
      h *= d;
    }
    return [w, h];
  }

  function _createYoutubeFrame(link, maxWidth) {
    let m;
    if (link.indexOf('youtu.be/') !== -1) {
      m = /youtu.be\/([-0-9a-z_]+)/i.exec(link);
    } else {
      m = /youtube(?:-nocookie)?.com\/watch\?(?:.*?&)?v=([-0-9a-z_]+)/i.exec(link);
    }
    const vid = m[1];
    const key = `oy${vid}`;
    const host = settingsGet(CONF_LINKS_YOUTUBE_ANON) ? 'youtube-nocookie' : 'youtube';
    return Promise.resolve(getCache(key)).then(function (cache) {
      if (cache != null) {
        return cache;
      }
      return request(`http${https}://www.youtube.com/oembed?url=http${https}://www.youtube.com/watch?v=${vid}`, 'GET', true)
    }).then(function (data) {
      let size = data;
      if (!Array.isArray(data)) {
        // save to cache
        size = [data.width|0, data.height|0];
        setCache(key, size);
        saveCache();
      }
      const adjusted = _adjustSize(size, maxWidth);
      let w = adjusted[0], h = adjusted[1];
      return $('<iframe>').attr({
        src: `https://www.${host}.com/embed/${vid}?feature=oembed`,
        width: `${Math.floor(w)}`,
        height: `${Math.floor(h)}`,
        frameborder: '0',
        allow: 'encrypted-media; autoplay; fullscreen'
      });
    });
  }

  function _createCoubFrame(link, maxWidth) {
    const m = /coub\.com\/(view|embed)\/([-0-9a-z_]+)/i.exec(link);
    const vid = m[2];
    const key = `oc${vid}`;
    return Promise.resolve(getCache(key)).then(function (cache) {
      if (cache != null) {
        return cache;
      }
      return request(`http${https}://coub.com/api/oembed.json?url=http${https}://coub.com/view/${vid}`, 'GET', true)
    }).then(function (data) {
      let size = data;
      if (!Array.isArray(data)) {
        // save to cache
        size = [data.width|0, data.height|0];
        setCache(key, size);
        saveCache();
      }
      const adjusted = _adjustSize(size, maxWidth, 301);
      let w = adjusted[0], h = adjusted[1];
      return $('<iframe>').attr({
        src: `http${https}://coub.com/embed/${vid}?muted=false&autostart=false&originalSize=false&startWithHD=false`,
        width: `${Math.floor(w)}`,
        height: `${Math.floor(h)}`,
        frameborder: '0',
        allowfullscreen: 'allowfullscreen',
      });
    });
  }

  function _youtubeSelectors () {
    return [
      '.MessageText a[href*="youtube.com/watch"]',
      '.MyCommentsItemText a[href*="youtube.com/watch"]',
      '.MessageText a[href*="https://youtu.be/"]',
      '.MyCommentsItemText a[href*="https://youtu.be/"]',
    ].join(', ');
  }

  function _coubSelectors () {
    return [
      '.MessageText a[href*="coub.com/view/"]',
      '.MyCommentsItemText a[href*="coub.com/view/"]',
    ].join(', ');
  }

  function reparseMessages(parent, preventPCL) {
    if (UID == null) {
      return;
    }
    pauseObserve();
    replaceImageLinks(parent);
    replaceSiteLinks(parent);
    fixMessageSpoilers(parent);
    parseTags(parent);
    parseTagStrike(parent);
    insertArchiveLinks(parent);
    if (preventPCL !== true) {
      parseCommentLinks(true);
    }
    resumeObserve(parent!=null);
  }

  function _findPlaceToInject($elem, stopClass) {
    let before = $elem.nextAll('br').get(0) || null;
    if (before != null) {
      do {
        const next = before.nextElementSibling;
        if (next == null || !next.classList.contains(stopClass)) {
          break;
        }
        before = next;
      } while (true);
    }
    return before != null ? before.nextSibling : null;
  }

  function _addLinkIcon(elem, icon) {
    const i = document.createElement('i');
    i.classList.add('fa');
    i.classList.add(icon);
    elem.insertBefore(i, elem.childNodes[0]);
    return i;
  }

  function replaceSiteLinks(parent) {
    if (UID == null) {
      return;
    }
    // TODO: fix non-ftf links - remove go.php redirect
    /*
    const search = new RegExp(`https?:\/\/${isOld?'':'old\.'}fanfics\.me`);
    const replace = `https://${!isOld?'':'old.'}fanfics.me`;
    $('.MessageText a[href*="fanfics.me"], .MyCommentsItemText a[href*="fanfics.me"]', parent).each(function(){
      if (this.classList.contains('ffme-parsed')) {
        return;
      }
      const self = this;
      const $self = $(self);
      const oldHref = this.getAttribute('href');
      const oldText = this.textContent;
      this.setAttribute('href', oldHref.replace(search, replace));
      this.textContent = oldText.replace(search, replace);
      this.classList.add('ffme-parsed');
      const $p = $self.closest('.MessageText, .MyCommentsItemText');
      const mid = $p.attr('id');
      _addEditHook(mid, { e: self, rc: 'ffme-parsed' });
      _addEditHook(mid, { e: self, a: 'href', v: oldHref });
      _addEditHook(mid, { e: self, t: oldText });
    });
    */
  }

  function replaceImageLinks(parent) {
    if (UID == null) {
      return;
    }
    const ls = settingsGet(CONF_IMAGE_LINKS_STATIC),
      la = settingsGet(CONF_IMAGE_LINKS_ANIMATED);

    if (ls || la) {
      $('.MessageText a[href], .MyCommentsItemText a[href]', parent).each(function(){
        const self = this;
        if (self.classList.contains('ffme-parsed')) {
          return;
        }
        const url = self.getAttribute('href');
        if (/\/uploads\.ru\//i.test(url)) {
          return;
      }
        if ((ls && /\.(jpe?g|png)$/i.test(url)) || (la && /\.gif$/i.test(url))) {
          const $self = $(self);
          const $p = $self.closest('.MessageText, .MyCommentsItemText');
          const mid = $p.attr('id');
          const txt = $self.text();
          const p = self.parentNode;
          const n = document.createElement('img');
          n.src = url;
          if (url.replace('/go.php?url=', '') !== txt) {
            n.classList.add('ffme--image');
            const before = _findPlaceToInject($self, 'ffme--image');
            p.insertBefore(n, before);
            const i = _addLinkIcon(self, 'fa-picture-o');
            self.classList.add('ffme-parsed');
            _addEditHook(mid, { e: n, d: true });
            _addEditHook(mid, { e: i, d: true });
            _addEditHook(mid, { e: self, rc: 'ffme-parsed' });
          } else {
            p.replaceChild(n, self);
            _addEditHook(mid, { e: n, r: self });
          }
        }
      });
    }

    const isAlone = /^\/message\d+/.test(window.location.pathname);

    if (settingsGet(CONF_LINKS_YOUTUBE)) {
      const _cnf_yt_p = settingsGet(CONF_LINKS_YOUTUBE_POPUP),
        _cnf_yt_a = settingsGet(CONF_LINKS_YOUTUBE_ALONE);
      $(_youtubeSelectors(), parent).each(function(){
        if (this.classList.contains('ffme-parsed')) {
          return;
        }
        const $a = $(this);
        const $p = $a.closest('.MessageText, .MyCommentsItemText');
        const mid = $p.attr('id');
        if (_cnf_yt_p && !(isAlone && _cnf_yt_a)) {
          const i = _addLinkIcon(this, 'fa-window-restore');
          this.classList.add('ffme-parsed');
          _addEditHook(mid, { e: i, d: true });
          _addEditHook(mid, { e: this, rc: 'ffme-parsed' });
        } else {
          const self = this;
          const p = self.parentNode;
          const url = self.getAttribute('href');
          const txt = $a.text();
          const texty = url.replace('/go.php?url=', '') !== txt;
          const mw = $p.get(0).getBoundingClientRect().width;
          if (texty) {
            self.classList.add('ffme-parsed');
          }
          _createYoutubeFrame(url, mw).then(function ($new) {
            const n = $new.get(0);
            if (texty) {
              n.classList.add('ffme--video');
              const before = _findPlaceToInject($a, 'ffme--video');
              p.insertBefore(n, before);
              const i = _addLinkIcon(self, 'fa-video-camera');
              _addEditHook(mid, { e: n, d: true });
              _addEditHook(mid, { e: i, d: true });
              _addEditHook(mid, { e: self, rc: 'ffme-parsed' });
            } else {
              p.replaceChild(n, self);
              _addEditHook(mid, { e: n, r: self });
            }
          }).catch(console.log.bind(console));
        }
      });
    }

    if (settingsGet(CONF_LINKS_COUB)) {
      const _cnf_cb_p = settingsGet(CONF_LINKS_COUB_POPUP),
        _cnf_cb_a = settingsGet(CONF_LINKS_COUB_ALONE);
      $(_coubSelectors(), parent).each(function(){
        if (this.classList.contains('ffme-parsed')) {
          return;
        }
        const $a = $(this);
        const $p = $a.closest('.MessageText, .MyCommentsItemText');
        const mid = $p.attr('id');
        if (_cnf_cb_p && !(isAlone && _cnf_cb_a)) {
          const i = _addLinkIcon(this, 'fa-window-restore');
          this.classList.add('ffme-parsed');
          _addEditHook(mid, { e: i, d: true });
          _addEditHook(mid, { e: this, rc: 'ffme-parsed' });
        } else {
          const self = this;
          const p = self.parentNode;
          const url = self.getAttribute('href');
          const txt = $a.text();
          const texty = url.replace('/go.php?url=', '') !== txt;
          const mw = $p.get(0).getBoundingClientRect().width;
          if (texty) {
            self.classList.add('ffme-parsed');
          }
          _createCoubFrame(url, mw).then(function ($new) {
            const n = $new.get(0);
            if (texty) {
              n.classList.add('ffme--video');
              const before = _findPlaceToInject($a, 'ffme--video');
              p.insertBefore(n, before);
              const i = _addLinkIcon(self, 'fa-video-camera');
              _addEditHook(mid, { e: n, d: true });
              _addEditHook(mid, { e: i, d: true });
              _addEditHook(mid, { e: self, rc: 'ffme-parsed' });
            } else {
              p.replaceChild(n, self);
              _addEditHook(mid, { e: n, r: self });
            }
          }).catch(console.log.bind(console));
        }
      });
    }
  }

  function fixMessageSpoilers(parent) {
    if (!settingsGet(CONF_SPOILER_COLLAPSE_HIDDEN)) {
      return;
    }
    $('div.spoiler', parent).closest('.MessageText').each(function () {
      const self = this;
      const b = self.querySelector('.MessageToShortLong');
      if (!b) {
        return;
      }
      b.style = 'display: none !important';
      const h = self.scrollHeight;
      if (h < 176) {
        b.parentNode.removeChild(b);
        self.parentNode.removeChild(self.nextElementSibling);
        self.style = null;
      } else {
        b.style = null;
      }
    });
  }

  function insertArchiveLinks(parent) {
    if (!settingsGet(CONF_BLOG_ARCHIVE_LINKS)) {
      return;
    }
    const $elems = parent != null ? $('.dropdown-click', parent) : $('.Message .dropdown-click');
    $elems.each(function () {
      const self = this;
      const $self = $(self);
      const $mr = $self.closest('.MessageRight');
      if ($mr.get(0).classList.contains('ffme--archive')) {
        return;
      }
      $mr.addClass('ffme--archive');
      const mid = $mr.find('.MessageText').attr('id').split('_').pop();
      const $before = $mr.find(isAlone && !isAloneOwn ? '.MessageButtons > .Management' : '.MessageButtons > .MessageManagementContainer > .MessageManagement').eq(0);
      const $a = $archive.clone();
      const $link = $a.find('a');
      $link.attr('href', $link.attr('href').replace('URL', encodeURIComponent(`${origin}/message${mid}?comment_id=1`)));
      if ($before.length) {
        if (isAlone && !isAloneOwn) {
          $a.insertAfter($before);
        } else {
          const $_after = $before.children('br').eq(0);
          $('<br>').insertAfter($_after);
          $link.insertAfter($_after);
        }
      } else {
        $mr.find('.MessageButtons').append($a);
      }
    });
  }

  function parseTags(parent) {
    if (!settingsGet(CONF_TAGS_PARSE)) {
      return;
    }
    const $p = $('.MessageText, .MyCommentsItemText', parent);
    const items = $p.contents(':contains(#)').filter(function () {
      return this.nodeType === 3 && this.parentNode.tagName.toLowerCase() !== 'a';
    }).get();

    const regstr = '^((?:.|[\\r\\n])*?)(#[а-яё\\w\\d_]+)((?:.|[\\r\\n])*?)$';
    for (let idx = 0; idx < items.length; idx++) {
      const e = items[idx];
      const re = new RegExp(regstr, 'igu');
      const m = re.exec(e.nodeValue) || null;
      if (m == null) {
        continue;
      }
      const parent = e.parentNode;
      if (m[3]) {
        const clone = e.cloneNode(false);
        clone.nodeValue = m[3];
        parent.insertBefore(clone, e.nextSibling);
        if (m[3].indexOf('#') !== -1) {
          items.push(clone);
        }
      }

      if (m[1]) {
        const clone = e.cloneNode(false);
        clone.nodeValue = m[1];
        parent.insertBefore(clone, e);
      }

      const n = document.createElement('a');
      n.setAttribute('href', '/blogs?search=%23' + m[2].substr(1));
      n.appendChild(document.createTextNode(m[2]));
      parent.replaceChild(n, e);
    }
  }

  function parseTagStrike(parent) {
    const $p = $('.MessageText, .MyCommentsItemText', parent);
    const items = $p.contents(':contains("[s]")').filter(function () {
      return this.nodeType === 3 && this.parentNode.tagName.toLowerCase() !== 'a';
    }).get();

    const regstr = '^((?:.|[\\r\\n])*?)\\[s\\]([^\\[\\r\\n]+)\\[\\/s\\]((?:.|[\\r\\n])*?)$';
    for (let idx = 0; idx < items.length; idx++) {
      const e = items[idx];
      const re = new RegExp(regstr, 'igu');
      const m = re.exec(e.nodeValue) || null;
      if (m == null) {
        continue;
      }
      const parent = e.parentNode;
      if (m[3]) {
        const clone = e.cloneNode(false);
        clone.nodeValue = m[3];
        parent.insertBefore(clone, e.nextSibling);
        if (m[3].indexOf('[s]') !== -1) {
          items.push(clone);
        }
      }

      if (m[1]) {
        const clone = e.cloneNode(false);
        clone.nodeValue = m[1];
        parent.insertBefore(clone, e);
      }

      const n = document.createElement('s');
      n.appendChild(document.createTextNode(m[2]));
      const o = document.createTextNode(`[s]${m[2]}[/s]`);
      const mid = parent.id;
      parent.replaceChild(n, e);
      _addEditHook(mid, { e: n, r: o });
    }
  }

  function parseCommentLinks(queued) {
    if (queued !== true) {
      pauseObserve();
    }
    $('[id^="MessageComments_"]').each(function () {
      const mid = this.id.split('_').pop();
      const msg = `/message${mid}`;
      $('.MessageRight:not([colspan]) > .small.light > a', `#${this.id}`).each(function () {
        if (!this.childNodes.length || this.classList.contains('ffme-parsed')) {
          return;
        }
        const span = this.childNodes[0];
        if (span.nodeType !== 1 || span.tagName.toLowerCase() !== 'span' || !span.classList.contains('uptodate')) {
          return;
        }
        const cid = this.href.replace(/[^0-9]/g, '');
        const txt = document.createTextNode(' | ');
        this.parentNode.append(txt);
        const link = document.createElement('a');
        link.href = `${msg}?comment_id=${cid}`;
        link.classList.add('light');
        link.title = 'Ссылка на комментарий в контексте обсуждения';
        _addLinkIcon(link, 'fa-link');
        this.parentNode.append(link);
        this.classList.add('ffme-parsed');
      });
    });
    if (queued !== true) {
      resumeObserve();
    }
  }

  function startObserver() {
    if (isFicWrite) return;
    const MutationObserver = win.MutationObserver || win.WebKitMutationObserver || win.MozMutationObserver;
    const elems = $.unique([
      $('#data-container').get(0),
      $('.ContentTable.Messages').get(0),
      $('.ContentTable').get(0),
      $('#Messages_container').get(0),
      $('.MessageAlone').get(0),
      $('#comments_list').get(0),
      $('.comments').get(0)
    ]);
    //const re1 = /^MessageComments_\d+$/;
    const observer = new MutationObserver(function (mutations) {
      if (preventObserve === true) {
        return;
      }
      const calls = {
        atb: false,
        pcl: false,
        rpm: false,
        ag: false,
        cb: false,
        msgs: [],
      };
      mutations.forEach(function (m) {
        const target = m.target;
        if (target != null && target.classList != null && target.classList.contains('TextFromWord2')) {
          return;
        }
        if (m.addedNodes != null && m.addedNodes.length) {
          // .every() polyfill
          let acc = m.addedNodes.length;
          m.addedNodes.forEach(function (n) {
            if (n.nodeType === 3 || n.classList.contains('ffe-blog-text-attr')) {
              acc--;
              return;
            }
            if (n.matches('.Message')) {
              calls.msgs.push(n);
            } else {
              const nc = n.querySelectorAll('.Message');
              if (nc.length) {
                calls.msgs.push.apply(calls.msgs, Array.from(nc));
              }
            }
          });
          if (m.target != null) {
            if (m.target.id === 'iknow_1') {
              calls.ag = true;
            }
            if (m.target.nodeType !== 3 && m.target.classList.contains('MessageText')) {
              calls.rpm = true;
            }
          }
          if (!acc) {
            return;
          }
          calls.atb = true;
          calls.pcl = true;
          calls.cb = true;
        }
      });
      if (calls.atb) {
        _attachTextButtons();
      }
      if (calls.pcl) {
        _parseCommentLinks();
      }
      if (calls.ag) {
        _attachAuthorGuesser();
      }
      if (calls.cb) {
        _attachCiteButtons();
      }
      if (calls.rpm) {
        reparseMessages(null, true);
      } else {
        if (calls.msgs.length) {
          const msgs = $.unique(calls.msgs);
          reparseMessages(msgs, true);
        }
      }
    });
    elems.forEach(function (elem) {
      if (elem != null) {
        observer.observe(elem, { childList: true, subtree: true });
      }
    });
  }

  function startFicWorkObserver() {
    if (!isFicWrite) return;
    const MutationObserver = win.MutationObserver || win.WebKitMutationObserver || win.MozMutationObserver;
    const elems = $.unique([
      document.querySelector('#site-content-center')
    ]);
    //const re1 = /^MessageComments_\d+$/;
    const observer = new MutationObserver(function (mutations) {
      if (preventObserve === true) {
        return;
      }
      const calls = {
        gdocs: false,
      };
      mutations.forEach(function (m) {
        const target = m.target;
        if (target != null && target.classList != null && target.classList.contains('TextFromWord2')) {
          return;
        }
        if (m.addedNodes != null && m.addedNodes.length) {
          // .every() polyfill
          let acc = m.addedNodes.length;
          m.addedNodes.forEach(function (n) {
            if (n.nodeType === 3 || n.classList.contains('ffme-parsed')) {
              acc--;
              return;
            }
            if (n.id === 'data-container') {
              calls.gdocs = true;
            }
          });
        }
      });
      if (calls.gdocs) {
        attachFicWriteButtons();
      }
    });
    elems.forEach(function (elem) {
      if (elem != null) {
        observer.observe(elem, { childList: true, subtree: false });
      }
    });
  }

  function attachAuthorGuesser() {
    if (!isFicHead && !isFicRead) return;

    const a = document.querySelector('#iknow_1 a.iknow');
    if (a == null || a.classList.contains('ffme-parsed')) return;

    a.classList.add('ffme-parsed');
    a.setAttribute('onclick', "$('#iknow_2').toggle();");

    const $sel = $('<select id="ffme-guess-author" name="author"><option></option></select>');
    const $txt = $('#iknow_2 input[type=text]');
    $sel.insertAfter($txt);
    $txt.remove();
    $sel.select2({
      placeholder: 'Найти пользователя',
      // minimumResultsForSearch: 1,
      minimumInputLength: 1,
      language: {
        inputTooShort: function(opts){ return `Введите хотя бы ${opts.minimum} символ`; },
        noResults: function(){ return 'Ничего не найдено'; },
        searching: function(){ return 'Поиск...'; },
      },
      templateResult: function (r) {
        if (r.loading) {
          return r.text;
        }
        if (r.type !== 'user') {
          return null;
        }
        return $(`<div class="ffme-results-user clearfix"><img src="${r.foto}"/><strong>${escapeMarkup(r.name)}</strong><span class="light">${escapeMarkup(r.desc)}</span></div>`);
      },
      ajax: {
        url: 'ajax_search.php',
        delay: LIVE_SEARCH_DELAY,
        dataType: 'json',
        type: 'POST',
        data: function (params) {
          return {
            t: 'user',
            q: params.term
          };
        },
        processResults: function (data) {
          if (data.error === '1') {
            return { results: [] };
          }
          data.forEach(function (d) {
            d.text = d.name;
            d.id = d.name;
          });
          return { results: data };
        },
      },
    });

  }

  function getUser() {
    const nameNode = document.querySelector('.topbar-pers-name');
    if (nameNode == null) {
      return;
    }
    let uidNode = nameNode;
    if (!isFicRead) {
      uidNode = document.querySelector('.main_menu2 > li.first > a');
    } else {
      uidNode = document.querySelector('.HeaderSlideMenu > ul > li.first > a')
    }

    userName = nameNode.textContent.trim();
    UID = uidNode.getAttribute('href').replace(/\/user/, '');
    isOwnProfile = isProfile && win.location.pathname === '/user'+UID;
  }

  function attachSettingsButton() {
    if (UID == null) {
      return;
    }
    const $cont = $('a.light[href="/user_properties"]').parent();
    if (!$cont.length) {
      return;
    }
    const $button = $('<input class="modern_button ffme_settings_btn" type="button" value="FFME">');
    $cont.prepend($button);
    const $support = $('a.light[href="/support"]').parent();
    if ($support.length) {
      const $sbtn = $('<a class="ffme_support_btn" href="/message98129"><i class="fa fa-comments"></i></a>');
      $support.prepend($sbtn);
    }
    $button.on('click', function (e) {
      e.preventDefault();
      $('#exampleModal').addClass('ffme').arcticmodal({
        openEffect: { type: 'none' }, closeEffect: { type: 'none' },
        closeOnOverlayClick: false,
        closeOnEsc: true,
        afterClose: function(){
          $('#exampleModal').removeClass('ffme');
        }
      });
      const oldSettings = {};
      Object.keys(DEFAULTS).forEach(function (k) {
        oldSettings[k] = settingsGet(k);
      });

      const $html = `
        <div class="modal-title">Настройки: ${userName}</div>
        <div class="HorMenuSecondLine clearfix ffme_settings_menu">
          <ul class="left">
            <li class="activ2"><a href="#pics">Картинки</a></li>
            <li><a href="#links">Внешние ссылки</a></li>
            <li><a href="#hls">Подсветка</a></li>
            <li><a href="#other">Разное</a></li>
            <li><a href="#design">Дизайн</a></li>
            <li><a href="#info">Инфо</a></li>
          </ul>
        </div>
        <div class="ffme_settings_blocks">
          <div class="ffme_settings_block active" data-rel="pics">
            <div class="checkbox light small ${oldSettings[CONF_IMAGE_TETHER]?'checked':''}" data-cnf="${CONF_IMAGE_TETHER}">Ограничивать размеры картинок в сообщениях</div>
            <div class="checkbox light small sub ${oldSettings[CONF_IMAGE_TETHER_SPOILER]?'checked':''}" data-cnf="${CONF_IMAGE_TETHER_SPOILER}">Только картинки в блоках</div>
            <div class="light small sub">
              <span>Максимальная ширина:</span><b data-from="${CONF_IMAGE_TETHER_WIDTH}">${oldSettings[CONF_IMAGE_TETHER_WIDTH]}</b>
              <input type="range" data-cnf="${CONF_IMAGE_TETHER_WIDTH}" min="50" max="750" step="5" value="${oldSettings[CONF_IMAGE_TETHER_WIDTH]}"/>
            </div>
            <div class="light small sub">
              <span>Максимальная высота:</span><b data-from="${CONF_IMAGE_TETHER_HEIGHT}">${oldSettings[CONF_IMAGE_TETHER_HEIGHT]}</b>
              <input type="range" data-cnf="${CONF_IMAGE_TETHER_HEIGHT}" min="50" max="750" step="5" value="${oldSettings[CONF_IMAGE_TETHER_HEIGHT]}"/>
            </div>
            <div class="checkbox light small ${oldSettings[CONF_IMAGE_ZOOM_BY_CLICK]?'checked':''}" data-cnf="${CONF_IMAGE_ZOOM_BY_CLICK}">Показывать оригинальный размер картинки по клику</div>
            <div class="checkbox light small ${oldSettings[CONF_IMAGE_LINKS_STATIC]?'checked':''}" data-cnf="${CONF_IMAGE_LINKS_STATIC}">Раскрывать ссылки на статичные картинки</div>
            <div class="checkbox light small ${oldSettings[CONF_IMAGE_LINKS_ANIMATED]?'checked':''}" data-cnf="${CONF_IMAGE_LINKS_ANIMATED}">Раскрывать ссылки на анимированные картинки</div>
          </div>
          <div class="ffme_settings_block" data-rel="links">
            <div class="checkbox light small ${oldSettings[CONF_LINKS_YOUTUBE]?'checked':''}" data-cnf="${CONF_LINKS_YOUTUBE}">Раскрывать ссылки на YouTube</div>
            <div class="checkbox light small sub ${oldSettings[CONF_LINKS_YOUTUBE_ANON]?'checked':''}" data-cnf="${CONF_LINKS_YOUTUBE_ANON}">Просматривать видео анонимно</div>
            <div class="checkbox light small sub ${oldSettings[CONF_LINKS_YOUTUBE_POPUP]?'checked':''}" data-cnf="${CONF_LINKS_YOUTUBE_POPUP}">Только во всплывающем окне</div>
            <div class="checkbox light small sub sub2 ${oldSettings[CONF_LINKS_YOUTUBE_ALONE]?'checked':''}" data-cnf="${CONF_LINKS_YOUTUBE_ALONE}">И при присмотре сообщения</div>
            <div class="checkbox light small ${oldSettings[CONF_LINKS_COUB]?'checked':''}" data-cnf="${CONF_LINKS_COUB}">Раскрывать ссылки на Coub</div>
            <div class="checkbox light small sub ${oldSettings[CONF_LINKS_COUB_POPUP]?'checked':''}" data-cnf="${CONF_LINKS_COUB_POPUP}">Только во всплывающем окне</div>
            <div class="checkbox light small sub sub2 ${oldSettings[CONF_LINKS_COUB_ALONE]?'checked':''}" data-cnf="${CONF_LINKS_COUB_ALONE}">И при просмотре сообщения</div>
          </div>
          <div class="ffme_settings_block" data-rel="hls">
            <div class="checkbox light small ${oldSettings[CONF_TAGS_HIGHLIGHT]?'checked':''}" data-cnf="${CONF_TAGS_HIGHLIGHT}">Подсвечивать тэги</div>
            <div class="checkbox light small ${oldSettings[CONF_TAGS_PARSE]?'checked':''}" data-cnf="${CONF_TAGS_PARSE}">Парсить и подсвечивать тэги в комментариях</div>
            <div class="checkbox light small ${oldSettings[CONF_LINKS_HIGHLIGHT]?'checked':''}" data-cnf="${CONF_LINKS_HIGHLIGHT}">Подсвечивать обычные ссылки (только цвет)</div>
            <div class="checkbox light small ${oldSettings[CONF_LINKS_FF_HIGHLIGHT]?'checked':''}" data-cnf="${CONF_LINKS_FF_HIGHLIGHT}">Подсвечивать ссылки на фики</div>
            <div class="checkbox light small ${oldSettings[CONF_LINKS_U_HIGHLIGHT]?'checked':''}" data-cnf="${CONF_LINKS_U_HIGHLIGHT}">Подсвечивать ссылки на пользователей (только цвет)</div>
            <div class="checkbox light small ${oldSettings[CONF_LINKS_R_HIGHLIGHT]?'checked':''}" data-cnf="${CONF_LINKS_R_HIGHLIGHT}">Подсвечивать ссылки на заявки</div>
          </div>
          <div class="ffme_settings_block" data-rel="other">
            <div class="checkbox light small ${oldSettings[CONF_SPOILER_COLLAPSE_HIDDEN]?'checked':''}" data-cnf="${CONF_SPOILER_COLLAPSE_HIDDEN}">Уменьшать высоту скрытого спойлера</div>
            <div class="checkbox light small ${oldSettings[CONF_ONELINE_ADDRESS]?'checked':''}" data-cnf="${CONF_ONELINE_ADDRESS}">Однострочное обращение по нику</div>
            <div class="checkbox light small ${oldSettings[CONF_FIX_LIKES_MODAL]?'checked':''}" data-cnf="${CONF_FIX_LIKES_MODAL}">Компактный вид окна мимимишек</div>
            <div class="checkbox light small ${oldSettings[CONF_KBD_NAV]?'checked':''}" data-cnf="${CONF_KBD_NAV}">Навигация клавиатурой по главам фика</div>
            <div class="checkbox light small ${oldSettings[CONF_NAV_SCROLL]?'checked':''}" data-cnf="${CONF_NAV_SCROLL}">Автоматически прокручивать к началу главы</div>
            <div class="checkbox light small ${oldSettings[CONF_BLOG_AUTO_DST]?'checked':''}" data-cnf="${CONF_BLOG_AUTO_DST}">Автоматически переключать приватность новых сообщений в блогах</div>
            <div class="checkbox light small ${oldSettings[CONF_BLOG_ARCHIVE_LINKS]?'checked':''}" data-cnf="${CONF_BLOG_ARCHIVE_LINKS}">Добавлять к сообщениям ссылки на архивацию</div>
            <div class="checkbox light small ${oldSettings[CONF_MOVE_BANNER]?'checked':''}" data-cnf="${CONF_MOVE_BANNER}">Передвигать вниз кнопку персонального баннера</div>
          </div>
          <div class="ffme_settings_block" data-rel="design">
            <div class="checkbox light small ${oldSettings[CONF_FULL_MENU]?'checked':''}" data-cnf="${CONF_FULL_MENU}">Всегда показывать все пункты выпадающего меню профиля</div>
            <div class="checkbox light small ${oldSettings[CONF_RICH_EDITOR]?'checked':''}" data-cnf="${CONF_RICH_EDITOR}">Расширенный редактор сообщений (бета)</div>
            <div class="light small sub hint ${oldSettings[CONF_RICH_EDITOR]?'':'hidden'}" data-show="${CONF_RICH_EDITOR}">Редактор доступен в общей ленте, личном профиле и на странице своих сообщений.</div>
          </div>
          <div class="ffme_settings_block" data-rel="info">
            <div class="light small ffme-info"><b>Fanfics Extender ${VERSION}</b></div>
            <div class="light small ffme-info">Кэш внешних ссылок (в байтах): ${JSON.stringify(CACHE).length}</div>
          </div>
        </div>
        <div class="modal-footer ffme-modal-footer">
          <input type="button" class="modern_button left ffme-save-settings" value="Сохранить" />
          <input type="button" class="modern_button_cancel right ffme-close-settings" value="Отмена" />
          <div class="clear"></div>
        </div>
      `;
      const $body = $('#modalBody').html($html);
      $('.checkbox', $body).on('click', function(){
        if (this.classList.contains('disabled')) return;
        this.classList.toggle('checked');
        const cnf = $(this).data('cnf');
        const val = this.classList.contains('checked');
        oldSettings[cnf] = val;
        $(`[data-show="${cnf}"]`, $body).each(function () {
          this.classList[val?'remove':'add']('hidden');
        });
      });
      $('[data-cnf]', $body).on('input', function(){
        const cnf = $(this).data('cnf');
        const val = this.value;
        oldSettings[cnf] = val;
        $(`[data-from="${cnf}"]`, $body).html(val);
      });
      const $optLinks = $('.HorMenuSecondLine a', $body);
      const $optBlocks = $('.ffme_settings_block', $body);
      $optLinks.on('click', function (e) {
        e.preventDefault();
        e.stopPropagation();
        const el = e.target;
        const li = el.parentNode;
        if (li.classList.contains('activ2')) {
          return;
        }
        const target = el.href.split('#').pop();
        $optLinks.parent().removeClass('activ2');
        li.classList.add('activ2');
        $optBlocks.removeClass('active').filter(`[data-rel="${target}"]`).addClass('active');
      });
      $('.ffme-close-settings').on('click', function (e) {
        e.preventDefault();
        $('#exampleModal').arcticmodal('close');
      });
      $('.ffme-save-settings').on('click', function (e) {
        e.preventDefault();
        Object.keys(oldSettings).forEach(function (k) {
          settingsSet(k, oldSettings[k]);
        });
        saveSettings();
        reparseMessages();
        $('#exampleModal').arcticmodal('close');
      });
    });
    // $cont.addClass('ffme_settings_btn_container');
  }

  const __texts__ = {
    hello: '[Привет-ведьма Фанфикса|https://fanfics.me/message168951]',
  };

  function _createClickHandler(inputSelector, symbols) {
    return function (ev) {
      ev.preventDefault();
      const $e = $(ev.target).closest('button');
      const tag = $e.data('ffe-tag');
      const input = $(inputSelector).get(0);
      const decor = $e.data('ffe-decor');
      if (decor != null && _hasSelection(input)) {
        _surroundBy(input, decor.substr(0,1), decor.substr(1,1), true);
      }
      else if (tag != null) {
        if (tag.substr(0,1) === '!') {
          const text = __texts__[tag.substr(1)];
          if (text) {
            input.value += text;
          }
        } else if (tag.length > 0) {
          _surroundWith(tag, input, symbols);
        } else {
          _clearFormat(input, symbols);
        }
      } else {
        let wasEmpty = !input.value.length;
        if (wasEmpty) {
          // insert space to prevent collapse
          input.value = ' ';
        }
        const modal = $e.data('ffe-modal');
        switch(modal) {
          case 'editor':
            _createModal_editor(input, wasEmpty);
            break;
          case 'link':
            _createModal_link(input, wasEmpty);
            break;
          case 'user':
            _createModal_user(input, wasEmpty);
            break;
          case 'story':
            _createModal_story(input, wasEmpty);
            break;
          case 'image':
            _createModal_image(input, wasEmpty);
            break;
        }
      }
    };
  }

  function _createModal_editor(input, wasEmpty) {
    $('#exampleModal').addClass('ffme ffme-editor').arcticmodal({
      openEffect: { type: 'none' }, closeEffect: { type: 'none' },
      closeOnOverlayClick: false,
      closeOnEsc: false,
      afterOpen: function(){
        inEditing = true;
      },
      beforeClose: function() {
        if (editor) {
          editor.model.document.off('change:data');
        }
        if (oldData !== false) {
          input.value = oldData;
        }
      },
      afterClose: function(){
        $('#exampleModal').removeClass('ffme ffme-editor');
        _defer(function () {
          if (wasEmpty) {
            input.value = '';
          }
          $input.trigger('textchange');
          input.focus();
          inEditing = false;
        });
      }
    });
    const $html = `
      <div class="modal-title">Редактор</div>
      <div class="ffme-editor-container"></div>
      <div class="modal-footer ffme-modal-footer">
        <input type="button" class="modern_button left ffme-modal-ok" value="Вставить" />
        <input type="button" class="modern_button_cancel right ffme-modal-close" value="Отмена" />
        <div class="clear"></div>
      </div>
    `;
    const $body = $('#modalBody').html($html);
    const editorElement = $body.find('.ffme-editor-container').get(0);
    let editor;
    let oldData = input.value;
    const $input = $(input);

    let tags = [];
    fetch('https://fanfics.me/section_message_post.php?action=myhashtags_toformnm')
      .then(r => r.json())
      .then((r) => {
        let t = '';
        try {
          t = r.q[0].a[0][0].valueOf();
        } catch(e) {
          return;
        }
        const c = document.createElement('div');
        c.innerHTML = t;
        Array.from(c.querySelectorAll('div.MessageNewMyhashtags > div'))
          .forEach((div) => {
            tags.push(div.querySelector('a').textContent.toLowerCase().trim());
          });
      });

    const getTags = function (query) {
      const s = query.toLowerCase();
      const list = tags.filter(t => ~t.indexOf(s));
      const ns = `#${s}`;
      if (s && !~list.indexOf(ns)) {
        list.unshift(ns);
      }
      return list;
    };

    let prevQuery = '';
    let prevUsers = [];
    const searchUsers = function (query) {
      if (query.length < 2) return [];
      query = query.trim();
      if (query === prevQuery) return prevUsers;
      prevQuery = query;
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          if (query !== prevQuery) {
            resolve(prevUsers);
          } else {
            fetch('ajax_search.php', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
              },
              body: (new URLSearchParams({ t: 'user', q: query })).toString()
            })
              .then(r => r.json())
              .then((r) => {
                if (r.error) return [];
                return r.filter(u => u.type === 'user').slice(0, 10);
              })
              .then((list) => {
                prevUsers = list.map((u) => {
                  u.id = `@${u.name}`;
                  return u;
                });
                resolve(prevUsers);
              });
          }
        }, 300);
      });
    };

    InlineEditor.create(editorElement, {
      mention: {
        feeds: [
          {
            marker: '#',
            feed: getTags,
          },
          {
            marker: '@',
            minimumCharacters: 2,
            feed: searchUsers,
            itemRenderer(item) {
              const el = document.createElement('span');
              el.classList.add('ck-user-mention');
              const img = document.createElement('img');
              img.src = item.foto;
              el.appendChild(img);
              const name = document.createElement('span');
              name.textContent = item.name;
              el.appendChild(name);
              return el;
            }
          }
        ]
      }
    }).then(ed => {
      editor = ed;
      if (!wasEmpty) {
        editor.setData(input.value);
      }
      editor.model.document.on('change:data', () => {
        _defer(() => {
          const data = editor.getData();
          input.value = data;
          $input.trigger('textchange');
        });
      });
      editor.model.change(function (writer) {
        writer.setSelection(writer.createPositionAt(editor.model.document.getRoot(), 'end'));
      });
      editor.editing.view.focus();
    }).catch(err => {
      console.error(err.stack);
    });

    const modalOk = function (e) {
      e.preventDefault();
      oldData = false;
      const data = editor.getData();
      if (data) {
        input.value = data;
        wasEmpty = false;
      } else {
        input.value = ' ';
        wasEmpty = true;
      }
      $('#exampleModal').arcticmodal('close');
    };
    $body.find('.ffme-modal-ok').on('click', modalOk);
  }

  function _createModal_link(input, wasEmpty) {
    $('#exampleModal').addClass('ffme').arcticmodal({
      openEffect: { type: 'none' }, closeEffect: { type: 'none' },
      closeOnOverlayClick: true,
      closeOnEsc: true,
      afterClose: function(){
        $('#exampleModal').removeClass('ffme');
        if (wasEmpty) {
          input.value = '';
        }
        input.focus();
      }
    });
    const hasSel = input.selectionStart !== input.selectionEnd;
    const $html = `
      <div class="modal-title">Вставить ссылку</div>
      <div class="ffme_settings_block active">
        <div class="light small clearfix">
          <input id="ffe-modal-field-url" type="text" class="input_3 input_modal" placeholder="Адрес ссылки" />
          ${hasSel?'':`
          <br/>
          <input id="ffe-modal-field-text" type="text" class="input_3 input_modal" placeholder="Текст ссылки" />
          `}
        </div>
      </div>
      <div class="modal-footer ffme-modal-footer">
        <input type="button" class="modern_button left ffme-modal-ok" value="Вставить" />
        <input type="button" class="modern_button_cancel right ffme-modal-close" value="Отмена" />
        <div class="clear"></div>
      </div>
    `;
    const $body = $('#modalBody').html($html);
    $body.find('#ffe-modal-field-url').select().focus();
    const modalOk = function (e) {
      e.preventDefault();
      const url = $.trim($('#ffe-modal-field-url').val());
      const text = $.trim($('#ffe-modal-field-text').val());
      if (url.length) {
        if (wasEmpty) {
          input.value = '';
          wasEmpty = false;
        }
        if (hasSel) {
          _surroundBy(input, '[', `|${url}]`, true);
        } else {
          _surroundBy(input, '', `[${text?text:url}|${url}]`, true);
        }
      }
      $('#exampleModal').arcticmodal('close');
    };
    $body.find('.ffme-modal-ok').on('click', modalOk);
    $body.find('.input_modal').on('keyup', function (e) {
      if (e.which === 13) {
        modalOk(e);
      }
    });
  }

  /*
  function _phePost(iframe, message) {
    iframe.contentWindow.postMessage(message, `http${https}://www.pichome.ru`);
  }
  */

  function _createModal_image(input, wasEmpty) {
    // let pheCheck = null;
    $('#exampleModal').addClass('ffme ffme-ii').arcticmodal({
      openEffect: { type: 'none' }, closeEffect: { type: 'none' },
      closeOnOverlayClick: true,
      closeOnEsc: true,
      afterClose: function(){
        // pheMessageHandler = null;
        // if (pheCheck) {
        //   clearTimeout(pheCheck); pheCheck = null;
        // }
        // $('#exampleModal').find('iframe').remove();
        $('#exampleModal').removeClass('ffme ffme-ii');
        if (wasEmpty) {
          input.value = '';
        }
        input.focus();
      }
    });
    const hasSel = input.selectionStart !== input.selectionEnd;
    const $html = `
      <div class="modal-title">Вставить изображение</div>
      <div class="HorMenuSecondLine clearfix ffme_settings_menu hidden">
        <ul class="left">
          <li class="activ2"><a href="#upload">Загрузить новое</a></li>
          <li><a href="#browse">Выбрать имеющееся</a></li>
        </ul>
      </div>
      <div class="ffme_settings_block active">
        <div class="ffme-insert-image phe-err">
<!--          <iframe frameborder="0" width="100%" height="460px" src="http${https}://www.pichome.ru/faq"></iframe>-->
<!--          <img src="/images/load_1.gif"/>-->
          <div class="phe-error">
            <br><h1><i class="fa fa-frown-o"></i></h1>
            <br><h2>К сожалению, поддержка <b>Pichome</b> прекращена.</h2>
            <br><p class="center">В ближайшем будущем будет добавлена поддержка сервиса <b>Imgur</b>.</p>
          </div>
        </div>
      </div>
    `;
    const $body = $('#modalBody').html($html);
    /*
    const $ii = $body.find('.ffme-insert-image');
    const phe = $body.find('iframe').get(0);
    pheMessageHandler = function (msg) {
      switch(msg.m) {
        case 'pong':
          if (pheCheck) {
            clearTimeout(pheCheck); pheCheck = null;
          }
          $ii.addClass('phe-ok');
          $body.find('.ffme_settings_menu').removeClass('hidden');
          break;
        case 'one':
          modalOk(msg.u);
          break;
      }
    };
    phe.addEventListener('load', function(){
      _phePost(phe, 'ping');
      pheCheck = setTimeout(function () {
        $ii.addClass('phe-err');
        pheCheck = null;
      }, 3000);
    });
    const $optLinks = $('.HorMenuSecondLine a', $body);
    $optLinks.on('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const el = e.target;
      const li = el.parentNode;
      if (li.classList.contains('activ2')) {
        return;
      }
      const target = el.href.split('#').pop();
      $optLinks.parent().removeClass('activ2');
      li.classList.add('activ2');
      _phePost(phe, target);
    });
    const modalOk = function (url) {
      if (url.length) {
        if (wasEmpty) {
          input.value = '';
          wasEmpty = false;
        }
        if (hasSel) {
          _surroundBy(input, '[', `|${url}]`, true);
        } else {
          _surroundBy(input, '', url, true);
        }
      }
      $('#exampleModal').arcticmodal('close');
    };
    */
  }

  function _createModal_user(input, wasEmpty) {
    $('#exampleModal').addClass('ffme').arcticmodal({
      openEffect: { type: 'none' }, closeEffect: { type: 'none' },
      closeOnOverlayClick: true,
      closeOnEsc: true,
      afterOpen: function(){
        _defer(function(){
          $select2.select2('open');
        });
      },
      afterClose: function(){
        $('#exampleModal').removeClass('ffme');
        if (wasEmpty) {
          input.value = '';
        }
        input.focus();
      }
    });
    const $html = `
      <div class="modal-title">Вставить ссылку на пользователя</div>
      <div class="ffme_settings_block active">
        <div class="ffme-search-user">
          <select id="ffme-search-user" class="select_modal"><option></option></select>
        </div>
      </div>
      <div class="modal-footer ffme-modal-footer">
        <input type="button" class="modern_button left ffme-modal-ok" value="Вставить" />
        <input type="button" class="modern_button_cancel right ffme-modal-close" value="Отмена" />
        <div class="clear"></div>
      </div>
      
    `;
    const $body = $('#modalBody').html($html);
    const $select2 = $body.find('#ffme-search-user').select2({
      placeholder: 'Найти пользователя',
      minimumResultsForSearch: 1,
      minimumInputLength: 1,
      language: {
        inputTooShort: function(opts){ return `Введите хотя бы ${opts.minimum} символ`; },
        noResults: function(){ return 'Ничего не найдено'; },
        searching: function(){ return 'Поиск...'; },
      },
      templateResult: function (r) {
        if (r.loading) {
          return r.text;
        }
        if (r.type !== 'user') {
          return null;
        }
        return $(`<div class="ffme-results-user clearfix"><img src="${r.foto}"/><strong>${escapeMarkup(r.name)}</strong><span class="light">${escapeMarkup(r.desc)}</span></div>`);
      },
      ajax: {
        url: 'ajax_search.php',
        delay: LIVE_SEARCH_DELAY,
        dataType: 'json',
        type: 'POST',
        data: function (params) {
          return {
            t: 'user',
            q: params.term
          };
        },
        processResults: function (data) {
          if (data.error === '1') {
            return { results: [] };
          }
          data.forEach(function (d) {
            d.text = d.name;
          });
          return { results: data };
        },
      },
    });
    const modalOk = function (e) {
      e.preventDefault();
      const uid = $select2.val();
      if (uid.length) {
        if (wasEmpty) {
          input.value = '';
          wasEmpty = false;
        }
        const item = $select2.select2('data').pop();
        _replaceWith(input, `{${item.name}}`);
      }
      $('#exampleModal').arcticmodal('close');
    };
    $body.find('.ffme-modal-ok').on('click', modalOk);
  }

  function _createModal_story(input, wasEmpty) {
    $('#exampleModal').addClass('ffme').arcticmodal({
      openEffect: { type: 'none' }, closeEffect: { type: 'none' },
      closeOnOverlayClick: true,
      closeOnEsc: true,
      afterOpen: function(){
        _defer(function () {
          $select2.select2('open');
        });
      },
      afterClose: function(){
        $('#exampleModal').removeClass('ffme');
        if (wasEmpty) {
          input.value = '';
        }
        input.focus();
      }
    });
    const $html = `
      <div class="modal-title">Вставить ссылку на фанфик</div>
      <div class="ffme_settings_block active">
        <div class="ffme-search-user">
          <select id="ffme-search-user" class="select_modal"><option></option></select>
        </div>
      </div>
      <div class="modal-footer ffme-modal-footer">
        <input type="button" class="modern_button left ffme-modal-ok" value="Вставить" />
        <input type="button" class="modern_button_cancel right ffme-modal-close" value="Отмена" />
        <div class="clear"></div>
      </div>
      
    `;
    const $body = $('#modalBody').html($html);
    const $select2 = $body.find('#ffme-search-user').select2({
      placeholder: 'Найти фанфик',
      minimumResultsForSearch: 1,
      minimumInputLength: 1,
      language: {
        inputTooShort: function(opts){ return `Введите хотя бы ${opts.minimum} символ`; },
        noResults: function(){ return 'Ничего не найдено'; },
        searching: function(){ return 'Поиск...'; },
      },
      templateResult: function (r) {
        if (r.loading) {
          return r.text;
        }
        return $(`<div class="ffme-results-story clearfix"><strong><span class="navy">${escapeMarkup(r.name)}</span> ${escapeMarkup(r.author)}</strong><span class="light">${r.desc}</span></div>`);
      },
      ajax: {
        url: 'ajax_search.php',
        delay: LIVE_SEARCH_DELAY,
        dataType: 'json',
        type: 'POST',
        data: function (params) {
          return {
            t: 'similar',
            q: params.term
          };
        },
        processResults: function (data) {
          if (data.error === '1') {
            return { results: [] };
          }
          data.forEach(function (d) {
            d.text = d.name;
          });
          return { results: data };
        },
      },
    });
    const modalOk = function (e) {
      e.preventDefault();
      const uid = $select2.val();
      if (uid.length) {
        if (wasEmpty) {
          input.value = '';
          wasEmpty = false;
        }
        const item = $select2.select2('data').pop();
        _replaceWith(input, `[${item.name}|${origin}/fic${item.id}]`);
      }
      $('#exampleModal').arcticmodal('close');
    };
    $body.find('.ffme-modal-ok').on('click', modalOk);
  }

  function _createButtons(inputSelector, attachBefore, onlyButtons) {
    let symbols = '[]';

    let hellowitch = false, ckeditor = false;
    if (onlyButtons) {
      if (onlyButtons.indexOf('*') === 0) {
        onlyButtons = onlyButtons.substr(1);
        symbols = '<>';
      } else if (onlyButtons.indexOf('+') === 0) {
        onlyButtons = false;
        hellowitch = true;
      } else if (onlyButtons.indexOf('!') === 0) {
        onlyButtons = false;
        ckeditor = true;
      }
    }
    const clickHandler = _createClickHandler(inputSelector, symbols);
    const ret = [];
    if (onlyButtons) {
      onlyButtons = onlyButtons.split(',');
    }
    if (!onlyButtons || onlyButtons.indexOf('b') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-bold"></i>').addClass('ffe-bold').prop('title', 'Жирный').data('ffe-tag', 'b').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('i') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-italic"></i>').addClass('ffe-italic').prop('title', 'Курсив').data('ffe-tag', 'i').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('s') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-strikethrough"></i>').addClass('ffe-strike').prop('title', 'Зачеркнутый').data('ffe-tag', 's').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('q') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-quote-right"></i>').addClass('ffe-quote').prop('title', 'Цитата').data('ffe-tag', 'q').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('sp') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-arrows-v"></i>').addClass('ffe-spoiler').prop('title', 'Спойлер').data('ffe-tag', 'sp').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('l') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-link"></i>').addClass('ffe-link').prop('title', 'Ссылка').data('ffe-modal', 'link').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('u') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-user"></i>').addClass('ffe-user').prop('title', 'Ссылка на пользователя').data({'ffe-modal': 'user', 'ffe-decor': '{}'}).on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('f') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-book"></i>').addClass('ffe-book').prop('title', 'Ссылка на фанфик').data({'ffe-modal': 'story', 'ffe-decor': '[]'}).on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('p') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-picture-o"></i>').addClass('ffe-image').prop('title', 'Изображение').data('ffe-modal', 'image').on('click', clickHandler)
      );
    }
    if (!onlyButtons || onlyButtons.indexOf('c') !== -1) {
      ret.push(
        $btn.clone().html('<i class="fa fa-eraser"></i>').addClass('ffe-eraser').prop('title', 'Очистить форматирование').data('ffe-tag', '').on('click', clickHandler)
      );
    }
    if (hellowitch) {
      ret.push(
        $btn.clone().html('<i class="fa fa-smile-o"></i>').addClass('ffe-hello').prop('title', 'Привет-ведьма').data('ffe-tag', '!hello').on('click', clickHandler)
      );
    }
    if (ckeditor && settingsGet(CONF_RICH_EDITOR)) {
      ret.push(
        $btn.clone().html('<i class="fa fa-magic"></i>').addClass('ffe-editor').prop('title', 'Редактор').data('ffe-modal', 'editor').on('click', clickHandler)
      );
    }
    if (!ret.length) {
      return;
    }
    if (attachBefore == null) {
      return ret;
    }
    ret.forEach(function ($b) {
      $b.insertBefore(attachBefore);
    });
  }

  function _hasSelection(input) {
    return input.selectionStart !== input.selectionEnd;
  }

  function _clearFormat(input, symbols) {
    const ss = input.selectionStart, se = input.selectionEnd, ot = input.value;
    if (ss !== se) {
      const re1 = new RegExp(`\\${symbols[0]}\\/?(b|i|q|sp?)\\${symbols[1]}`, 'g');
      const re2 = new RegExp(`\\${symbols[0]}q[^\\${symbols[1]}]*\\${symbols[1]}`, 'g');
      const nt = ot.substring(ss, se).replace(re1, '').replace(re2, '');
      input.value = `${ot.substring(0, ss)}${nt}${ot.substring(se)}`;
      input.setSelectionRange(ss, ss + nt.length);
    }
    input.focus();
  }

  function _surroundWith(tag, input, symbols) {
    const ss = input.selectionStart, se = input.selectionEnd, ot = input.value;
    const openTag = `${symbols[0]}${tag}${symbols[1]}`, closeTag = `${symbols[0]}/${tag}${symbols[1]}`;
    if (ot.substr(ss - openTag.length, openTag.length) === openTag && ot.substr(se, closeTag.length) === closeTag) {
      input.value = `${ot.substring(0, ss - openTag.length)}${ot.substring(ss, se)}${ot.substring(se + closeTag.length)}`;
      input.setSelectionRange(ss - openTag.length, se - openTag.length);
    } else if (ot.substr(ss, openTag.length) === openTag && ot.substr(se - closeTag.length, closeTag.length) === closeTag) {
      // do nothing
    } else {
      input.value = `${ot.substring(0, ss)}${openTag}${ot.substring(ss, se)}${closeTag}${ot.substring(se)}`;
      input.setSelectionRange(ss + openTag.length, se + openTag.length);
    }
    input.focus();
  }

  function _surroundBy(input, before, after, selAfter) {
    const ss = input.selectionStart, se = input.selectionEnd, ot = input.value;
    input.value = `${ot.substring(0, ss)}${before}${ot.substring(ss, se)}${after}${ot.substring(se)}`;
    if (selAfter) {
      const pos = se + before.length + after.length;
      input.setSelectionRange(pos, pos);
    } else {
      input.setSelectionRange(ss + before.length, se + before.length);
    }
    input.focus();
  }

  function _replaceWith(input, text) {
    const ss = input.selectionStart, se = input.selectionEnd, ot = input.value;
    input.value = `${ot.substring(0, ss)}${text}${ot.substring(se)}`;
    const pos = ss + text.length;
    input.setSelectionRange(pos, pos);
    input.focus();
  }

  function _defer(func) {
    setTimeout(func, 1);
  }
  win.eval(`_defer = ${_defer.toString()}`);

  function _debounce(func, wait) {
    let timeout, args, context, lastCallTime;

    const later = function () {
      const last = new Date().getTime() - lastCallTime;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        func.apply(context, args);
        context = args = null;
      }
    };

    return function () {
      context = this;
      args = arguments;
      lastCallTime = new Date().getTime();
      if (!timeout) {
        timeout = setTimeout(later, wait);
      }
    };
  }
  win.eval(`_debounce = ${_debounce.toString()}`);

  function pauseObserve() {
    preventObserve = true;
  }
  win.eval(pauseObserve.toString());

  function resumeObserve(immediately) {
    if (immediately) {
      preventObserve = false;
    } else {
      _defer(function () { preventObserve = false; });
    }
  }
  win.eval(resumeObserve.toString());

  function settingsGet(key) {
    const val = SETTINGS[key];
    return val != null ? val : DEFAULTS[key];
  }

  function settingsSet(key, val, save) {
    SETTINGS[key] = val;
    if (save === true) {
      saveSettings();
    }
  }

  function _updateWindowSettings() {
    const settings = {};
    Object.keys(DEFAULTS).forEach(function (k) {
      settings[k] = SETTINGS[k] != null ? SETTINGS[k] : DEFAULTS[k];
    });
    win.eval(`ffme_settings = ${JSON.stringify(settings)}`);
  }

  function getCache(id) {
    return CACHE[id] || null;
  }

  function setCache(id, value) {
    CACHE[id] = value;
    saveCache();
  }

  function loadSettings() {
    if (UID == null) {
      return;
    }
    const key = `ffme_${UID}`;
    try {
      SETTINGS = JSON.parse(win.localStorage.getItem(key) || '{}');
      if (SETTINGS['image_tether_clicking'] != null) {
        delete SETTINGS['image_tether_clicking'];
      }
      if (SETTINGS['nd_redirect'] != null) {
        delete SETTINGS['nd_redirect'];
      }
      if (SETTINGS['nd_redirect_links'] != null) {
        delete SETTINGS['nd_redirect_links'];
      }
      _updateWindowSettings();
    } catch(err) {
      // settings is corrupted
      win.localStorage.removeItem(key);
      SETTINGS = {};
    }
  }

  function saveSettings() {
    if (UID == null) {
      return;
    }
    const key = `ffme_${UID}`;
    win.localStorage.setItem(key, JSON.stringify(SETTINGS));
    _updateWindowSettings();
    updateSettingsCss();
    updateCustomSettings();
  }

  function loadCache() {
    if (UID == null) {
      return;
    }
    const key = `ffme_cache`;
    try {
      CACHE = JSON.parse(win.localStorage.getItem(key) || '{}');
    } catch(err) {
      // settings is corrupted
      win.localStorage.removeItem(key);
      CACHE = {};
    }
  }

  function saveCache() {
    if (UID == null) {
      return;
    }
    const key = `ffme_cache`;
    win.localStorage.setItem(key, JSON.stringify(CACHE));
  }

})(window, jQuery);